Skip to content

Self-Hosting

This guide walks you through running the Zirzir server on your own infrastructure. The server gives you a dashboard, webhook infrastructure, multi-project support, and a unified API your SDKs talk to.


The fastest path to a running server:

Terminal window
docker run -d \
--name zirzir \
-p 8080:8080 \
-v zirzir-data:/app/data \
-e ZIRZIR_SECURITY_ADMIN_API_KEY=$(openssl rand -hex 32) \
ghcr.io/recite-labs/zirzir/server:latest

Open http://localhost:8080 to access the dashboard.


For a more manageable setup, create a docker-compose.yml:

services:
zirzir:
image: ghcr.io/recite-labs/zirzir/server:latest
ports:
- "8080:8080"
volumes:
- zirzir-data:/app/data
- ./plugins:/app/plugins:ro
environment:
ZIRZIR_LOG_LEVEL: info
ZIRZIR_SECURITY_ADMIN_API_KEY: ${ZIRZIR_ADMIN_API_KEY}
ZIRZIR_SECURITY_ENCRYPTION_KEY: ${ZIRZIR_ENCRYPTION_KEY}
ZIRZIR_SERVER_ALLOW_ORIGINS: "https://yoursite.com"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
volumes:
zirzir-data:
Terminal window
# Generate secrets
echo "ZIRZIR_ADMIN_API_KEY=$(openssl rand -hex 32)" >> .env
echo "ZIRZIR_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
# Start
docker compose up -d

Requirements: Go 1.24+, Node.js 20+

Terminal window
git clone https://github.com/recite-labs/zirzir.git
cd zirzir/server
# Install dependencies and build (includes React dashboard)
make build
# Run
./bin/zirzir

For development with hot reload:

Terminal window
# Install air (hot reload tool)
go install github.com/air-verse/air@latest
# Run in dev mode
make dev

Zirzir loads configuration from three sources (in order of priority):

  1. Environment variables — prefixed with ZIRZIR_ (e.g. ZIRZIR_SERVER_PORT)
  2. Config fileconfig.yaml in the working directory, /etc/zirzir/, or ~/.zirzir/
  3. Defaults

Copy the example and edit:

Terminal window
cp config.example.yaml config.yaml
server:
port: 8080
host: 0.0.0.0
allow_origins:
- "https://yoursite.com"
database:
path: zirzir.db
log:
level: info # debug, info, warn, error
plugins:
directory: plugins
security:
encryption_key: "" # openssl rand -hex 32
admin_api_key: "" # openssl rand -hex 32

All config values can be set via environment variables using the ZIRZIR_ prefix with underscores replacing dots:

VariableConfig equivalentDefaultDescription
ZIRZIR_SERVER_PORTserver.port8080HTTP port
ZIRZIR_SERVER_HOSTserver.host0.0.0.0Bind address
ZIRZIR_SERVER_ALLOW_ORIGINSserver.allow_origins*CORS origins
ZIRZIR_DATABASE_PATHdatabase.pathzirzir.dbSQLite file path
ZIRZIR_LOG_LEVELlog.levelinfoLog verbosity
ZIRZIR_PLUGINS_DIRECTORYplugins.directorypluginsPlugin binary directory
ZIRZIR_SECURITY_ENCRYPTION_KEYsecurity.encryption_keyAES key for credential encryption
ZIRZIR_SECURITY_ADMIN_API_KEYsecurity.admin_api_keyKey for /admin endpoints

Zirzir uses SQLite — no external database server needed. Migrations run automatically on startup.

  • Default path: zirzir.db (current directory)
  • Docker path: /app/data/zirzir.db (inside the named volume)

To run migrations manually:

Terminal window
# Requires goose: go install github.com/pressly/goose/v3/cmd/goose@latest
goose -dir migrations sqlite3 zirzir.db up

SQLite databases are single files. Back up by copying:

Terminal window
# While the server is running (SQLite handles concurrent reads)
sqlite3 zirzir.db ".backup /backups/zirzir-$(date +%Y%m%d).db"

After the server starts:

Navigate to http://localhost:8080. You’ll be prompted to create an admin account.

A project is an isolated workspace with its own API keys and provider configurations. Create one from the dashboard or via the admin API:

Terminal window
ADMIN_KEY="your-admin-api-key"
# Create a merchant
curl -X POST http://localhost:8080/admin/merchants \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My App", "webhook_url": "https://yoursite.com/webhooks/zirzir"}'

Add Chapa credentials to your project — from the dashboard or via API:

Terminal window
MERCHANT_ID="merchant-id-from-step-2"
curl -X POST http://localhost:8080/admin/merchants/$MERCHANT_ID/providers \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "chapa",
"environment": "test",
"config": {
"secret_key": "CHASECK_TEST-..."
}
}'
Terminal window
curl -X POST http://localhost:8080/admin/merchants/$MERCHANT_ID/api-keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"environment": "test", "label": "Backend"}'

The response includes your zz_test_... API key. Use it in your SDK:

const zirzir = new Zirzir({
baseUrl: 'http://localhost:8080',
apiKey: 'zz_test_...',
})

External payment providers are loaded as plugins from the plugins/ directory.

Terminal window
# Build the example M-Pesa plugin
make plugin-example
# The binary is placed in plugins/
ls plugins/
# mpesa

Plugins are discovered automatically on startup. See Adding Providers for how to build your own.


Before going live:

  • Set admin_api_key — generate with openssl rand -hex 32
  • Set encryption_key — encrypts stored provider credentials
  • Restrict CORS — set allow_origins to your actual domains
  • Use a volume for the SQLite database (Docker: named volume, bare metal: persistent disk)
  • Set up backups — scheduled SQLite backup to object storage
  • Put behind a reverse proxy — Nginx or Caddy for TLS termination
  • Set log.level to warn in production (reduce noise)
  • Monitor /health — wire it to your uptime checker
pay.yourcompany.com {
reverse_proxy localhost:8080
}
server {
listen 443 ssl;
server_name pay.yourcompany.com;
ssl_certificate /etc/letsencrypt/live/pay.yourcompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pay.yourcompany.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}