Skip to main content

Self-Hosting Guide

Deploy PocketPing on your own infrastructure for complete control over your data.

Architecture Overview

A self-hosted PocketPing setup consists of three components:

ComponentDescription
Chat WidgetEmbedded on your website, connects via HTTP + SSE
Bridge ServerRoutes messages between widget and platforms
Messaging PlatformsTelegram, Discord, or Slack for notifications

Option 1: Minimal Setup

The simplest self-hosted setup uses the Python or Node.js SDK with embedded bridge support. No separate bridge server needed.

npm install @pocketping/sdk-node
server.js
const express = require('express');
const { PocketPing, TelegramBridge } = require('@pocketping/sdk-node');

const app = express();
const pp = new PocketPing({
bridges: [new TelegramBridge(process.env.TELEGRAM_BOT_TOKEN, process.env.TELEGRAM_CHAT_ID)],
});

// Mount routes
app.use('/pocketping', pp.middleware());

app.listen(8000);

Option 2: Full Setup with Bridge Server

For production or when you want to use multiple bridges, run the bridge server separately using Docker.

1. Deploy Bridge Server

The bridge server is written in Go and provides HTTP-only communication with messaging platforms.

docker-compose.yml
services:
bridge:
image: ghcr.io/pocketping/pocketping-bridge:latest
ports:
- "3001:3001"
environment:
- TELEGRAM_BOT_TOKEN=your_token
- TELEGRAM_CHAT_ID=your_chat_id
- DISCORD_BOT_TOKEN=your_discord_token
- DISCORD_CHANNEL_ID=your_channel_id
restart: unless-stopped

# Run: docker compose up -d

2. Point the Widget at the Bridge Server

The standalone bridge server is self-contained — there is no separate backend or SDK in between. Your widget connects to it directly via its endpoint. Expose the bridge on a public HTTPS URL (e.g. behind a reverse proxy) and use that URL:

<script src="https://cdn.pocketping.io/widget.js"></script>
<script>
PocketPing.init({
endpoint: 'https://bridge.yourdomain.com', // your bridge server URL
operatorName: 'Support',
});
</script>

That's it — open your site, send a message, and it appears in your messaging platform.

Storage Options

By default, sessions and messages are stored in memory using MemoryStorage. This works for development but data is lost on restart.

Built-in Storage

StorageIncludedDescription
MemoryStorage✅ YesIn-memory, data lost on restart. Good for dev/testing.
PostgresStorage❌ NoImplement yourself using the interface below.
RedisStorage❌ NoImplement yourself using the interface below.

Custom Storage Interface

To persist data, implement the Storage interface:

import { Storage } from '@pocketping/sdk-node';

class MyStorage implements Storage {
async createSession(session: Session): Promise<void>;
async getSession(sessionId: string): Promise<Session | null>;
async updateSession(session: Session): Promise<void>;
async deleteSession(sessionId: string): Promise<void>;
async saveMessage(message: Message): Promise<void>;
async getMessages(sessionId: string, after?: string, limit?: number): Promise<Message[]>;
async getMessage(messageId: string): Promise<Message | null>;
}

Example: PostgreSQL

note

This is an example. PostgresStorage is not included in the SDK—you need to implement it.

import { Storage, Session, Message } from '@pocketping/sdk-node';
import { Pool } from 'pg';

class PostgresStorage implements Storage {
private pool: Pool;

constructor(connectionString: string) {
this.pool = new Pool({ connectionString });
}

async createSession(session: Session): Promise<void> {
await this.pool.query(
'INSERT INTO sessions (id, visitor_id, created_at, last_activity) VALUES ($1, $2, $3, $4)',
[session.id, session.visitorId, session.createdAt, session.lastActivity]
);
}

async getSession(sessionId: string): Promise<Session | null> {
const result = await this.pool.query(
'SELECT * FROM sessions WHERE id = $1',
[sessionId]
);
if (result.rows.length === 0) return null;
const row = result.rows[0];
return {
id: row.id,
visitorId: row.visitor_id,
createdAt: row.created_at,
lastActivity: row.last_activity,
// ... other fields
};
}

// ... implement remaining methods
}

// Usage
const storage = new PostgresStorage('postgresql://user:pass@localhost/pocketping');

const pp = new PocketPing({
storage,
bridges: [new TelegramBridge(process.env.TELEGRAM_BOT_TOKEN, process.env.TELEGRAM_CHAT_ID)],
});

Deployment Checklist

  • Backend deployed with SSL (HTTPS)
  • Bridge server deployed (Docker or embedded)
  • At least one bridge configured (Telegram/Discord/Slack)
  • Widget added to frontend
  • CORS configured (backend allows widget domain)
  • Persistent storage configured (optional but recommended)
  • Health checks and monitoring in place

Environment Variables

VariableRequiredDescription
PORTNoBridge server port (default: 3001)
API_KEYNoSecret key for API authentication
TELEGRAM_BOT_TOKENIf using TelegramBot token from BotFather
TELEGRAM_CHAT_IDIf using TelegramTelegram supergroup ID (starts with -100)
DISCORD_BOT_TOKENIf using DiscordDiscord bot token
DISCORD_CHANNEL_IDIf using DiscordDiscord channel ID for threads
EVENTS_WEBHOOK_URLNoURL to forward custom events (Zapier, Make, n8n)
EVENTS_WEBHOOK_SECRETNoSecret for HMAC-SHA256 signature verification

Webhook Integration

Forward custom events to external services for automation:

docker-compose.yml
services:
bridge:
image: ghcr.io/pocketping/pocketping-bridge:latest
ports:
- "3001:3001"
environment:
- TELEGRAM_BOT_TOKEN=your_token
- TELEGRAM_CHAT_ID=your_chat_id
# Forward events to Zapier, Make, n8n, etc.
- EVENTS_WEBHOOK_URL=https://hooks.zapier.com/hooks/catch/123456/abcdef
- EVENTS_WEBHOOK_SECRET=your_secret_key # Optional

See Node.js SDK - Webhook Forwarding for payload structure and signature verification.

Next Steps