Configuration
pingd reads its configuration from environment variables. There is no config file. Defaults are tuned for local development; production deployments need to set at least an admin password, a data directory, and (for browser pushes) a VAPID config.
Core
The repository also includes .env.example with the same variables in copyable form.
| Variable | Default | Purpose |
|---|---|---|
ADMIN_USERNAME | admin | Admin username, seeded on first run |
ADMIN_PASSWORD | changeme | Admin password, seeded on first run |
PINGD_DATA_DIR | data | Directory for SQLite database and CLI config |
PINGD_ALLOW_REGISTRATION | false | If true, exposes POST /auth/register |
PINGD_GUEST_ENABLED | true | If false, disables POST /auth/guest |
PINGD_RESERVED_TOPIC_NAMES | unset | Comma-separated topic names that users cannot create; case-insensitive |
PINGD_DEFAULT_PUBLIC_READ | false | Default publicRead for newly created topics when omitted |
PINGD_DEFAULT_PUBLIC_PUBLISH | false | Default publicPublish for newly created topics when omitted |
PINGD_DEFAULT_SHARE_TOKEN_TTL | unset | Default share-token expiry when expiresAt is omitted; accepts 30d, 12h, 5m, 60s, or seconds |
PINGD_DEFAULT_PERMISSION_TTL | unset | Default permission expiry when expiresAt is omitted; same duration format |
PINGD_MAX_TOPICS_PER_USER | unset | Maximum topics per non-admin user; unset means unlimited |
PINGD_MAX_SHARE_TOKENS_PER_TOPIC | unset | Maximum active share tokens per topic; unset means unlimited |
PINGD_LOG_FORMAT | text | text or json |
LOG_LEVEL | environment | Vapor log level (trace, debug, info, ...) |
ADMIN_PASSWORD on first boot. It is only used to seed the very
first user; once the database exists, changing it has no effect. Rotate the admin password through the dashboard or API.
HTTP
pingd listens on port 7685 by default. The port is fixed at startup;
change the host port mapping (-p 8080:7685) if you want to expose it differently.
| Variable | Default | Purpose |
|---|---|---|
PORT | 7685 | HTTP listen port inside the container/process |
PINGD_CORS_ORIGIN | * | * or comma-separated list of allowed origins |
PINGD_RATE_LIMIT_COUNT | 30 | API requests/minute per IP |
PINGD_PUBLISH_RATE_LIMIT_PER_USER_PER_MIN | unset | Publish requests/minute per authenticated user; unset means unlimited |
PINGD_ANON_PUBLISH_RATE_LIMIT_PER_IP_PER_MIN | unset | Publish requests/minute per anonymous client IP; unset means unlimited |
PINGD_WEBHOOK_RATE_LIMIT_PER_TOKEN | 120 | Webhook receives/minute per token |
PINGD_WEBHOOK_RATE_LIMIT_PER_IP | 300 | Webhook receives/minute per IP |
Allowed CORS request headers: Authorization, Content-Type,
Origin, X-Requested-With, X-Topic-Token, and X-Push-Token.
APNS (iOS push)
APNS can run in either mode:
direct: this server holds the Apple.p8key and sends pushes itself.relay: this server forwards pushes to another pingd server that has the key.
The simplest setup is to point at a relay you trust:
PINGD_APNS_MODE=relay
PINGD_APNS_RELAY_BASE_URL=https://pingd.dev
PINGD_APNS_RELAY_TOKEN=<bearer token>
Leave PINGD_APNS_MODE unset for the mock provider, which logs pushes but sends nothing. Full walkthrough in APNS.
| Variable | Mode | Purpose |
|---|---|---|
PINGD_APNS_MODE | both | direct, relay, or unset for mock |
PINGD_APNS_KEY_PATH | direct | Path to Apple .p8 key file |
PINGD_APNS_KEY_ID | direct | Apple key ID |
PINGD_APNS_TEAM_ID | direct | Apple team ID |
PINGD_APNS_BUNDLE_ID | direct | App bundle identifier |
PINGD_APNS_ENV | direct | development or production (default) |
PINGD_APNS_RELAY_BASE_URL | relay | Base URL of the upstream pingd in direct mode |
PINGD_APNS_RELAY_TOKEN | relay | Bearer token for the relay server |
Web Push (browsers)
| Variable | Purpose |
|---|---|
PINGD_WEBPUSH_VAPID_CONFIG | JSON with contactInformation, primaryKey, expirationDuration, validityDuration |
Generate the value once per deployment with the bundled keygen:
docker compose exec pingd ./pingd-webpush-keygen --email admin@example.com
swift run pingd-webpush-keygen --email admin@example.com
When this variable is unset, GET /webpush/vapid-key returns 404 and
Web Push deliveries are treated as no-op deliveries.
Full setup in Web Push.
Logging
Request logs include request ID, method, path, status, and duration. Audit logs cover security-sensitive events: login, guest creation, user changes, topic changes, device registration, subscriptions, permissions, token changes, and webhook activity.
Use JSON logs in production:
PINGD_LOG_FORMAT=json
LOG_LEVEL=info
Reverse proxy setup
pingd serves plain HTTP and does not terminate TLS. In production, run it behind a
reverse proxy that handles TLS and forwards requests to pingd. The proxy must send a
clean client IP in X-Real-IP or X-Forwarded-For so pingd's
rate limiter sees the real client instead of the proxy.
Below are minimal working configs for Caddy and Nginx. Adapt hostnames, ports, and TLS paths to your deployment.
Caddy
pingd.example.com {
reverse_proxy localhost:7685 {
header_up X-Real-IP {remote_host}
flush_interval -1
transport http {
read_timeout 1h
}
}
}
Nginx
upstream pingd {
server 127.0.0.1:7685;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name pingd.example.com;
ssl_certificate /etc/letsencrypt/live/pingd.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pingd.example.com/privkey.pem;
location / {
proxy_pass http://pingd;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Connection "";
# SSE: keep the stream open and unbuffered
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
}
server {
listen 80;
server_name pingd.example.com;
return 301 https://$host$request_uri;
}
What pingd needs from the proxy
- No response buffering on streaming routes. SSE flushes one event at a time; buffering breaks live tailing.
- Long read timeouts (1 hour or more). Idle SSE connections time out under default proxy settings.
- HTTP/1.1 to upstream so streaming works.
- Clean client IP headers. Set
X-Real-IPorX-Forwarded-Forfrom the proxy's observed remote address. pingd ignoresForwardedand other vendor IP headers.
Rate limiting
Rate limits are configured with the variables above. The general API limit is per client IP. Publish and webhook routes have separate limits so they can be tuned without changing the rest of the API.