Two supported paths: DigitalOcean App Platform (managed, git-deploy, recommended) and any Docker host (DigitalOcean Droplet, EC2, bare metal — docker-compose.prod.yml is ready).
The spec at .do/app.yaml declares four components:
| Component | Kind | Purpose |
|---|---|---|
api |
Service (Dockerfile) | Node/Express — attribution, auth, network, webhooks |
router |
Service (Dockerfile) | Hono click redirect — the /r/<key> hot path |
portal |
Static site | Vite SPA, served from DO's CDN |
openpartner-db |
Managed Postgres | App-owned Postgres 16 |
Ingress rules fan incoming traffic out: /api/* → api, everything else → portal. The router runs on its own subdomain.
- Push the repo to GitHub (private is fine).
- Create the app, either via the DO dashboard (App Platform → Create App → GitHub → pick this repo → App Platform will detect
.do/app.yaml) or viadoctl:doctl apps create --spec .do/app.yaml
- Set the secrets App Platform marked as
SECRETin the spec:ADMIN_API_KEY— bootstrap admin token. Generate withnode -e "console.log('op_' + require('crypto').randomBytes(24).toString('hex'))".PORTAL_URL— e.g.https://partners.yourdomain.com. Required in production (CORS allowlist + invite email links).SECRETS_ENCRYPTION_KEY— 32 bytes (hex or base64) used to encrypt SMTP passwords / Postmark tokens stored in the Config table. Required in production. Generate withnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))".POSTMARK_SERVER_TOKEN,MAIL_FROM(orSMTP_HOST+ SMTP vars, or neither) — mail provider fallback. You can skip these entirely and let the admin configure mail from the Settings UI after install; these env vars win only when UI settings are empty.STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_FLAT_PRICE_ID— only if runningflatorrevsharemode.COOKIE_DOMAIN—.yourdomain.comso the router's_crefcookie covers your landing pages.METRICS_TOKEN— optional; set to require Bearer auth on/metrics.
- Deploy. The API container runs migrations on first boot via
apps/api/docker-entrypoint.sh, so an empty Postgres bootstraps automatically. Subsequent deploys re-run and are idempotent againstknex_migrations. - Wire custom domains. In the App Platform UI → Settings → Domains:
- Primary:
partners.yourdomain.com→portalcomponent. - Alias:
go.yourdomain.com→routercomponent.
- Primary:
Every SECRET env var lives in App Platform's secret store (encrypted at rest). Rotate via UI or:
doctl apps update <APP_ID> --spec .do/app.yamlDATABASE_URL is injected automatically from the managed Postgres component — don't set it manually.
The spec defaults to production: false (dev-tier Postgres). For real traffic:
- Edit
.do/app.yaml: setproduction: true, bumpnum_nodes, pick a bigger size. - Redeploy. DO migrates the dataset.
Or attach an externally-managed Postgres by removing the databases block and hard-coding DATABASE_URL as a secret.
- Logs: DO dashboard → Runtime Logs, or
doctl apps logs <APP_ID> --component api. - Metrics: built-in CPU / memory / req-rate panels per component.
- Health: every component has an HTTP healthcheck wired into the spec.
If you'd rather run the full stack on a single box:
# On your target host (Ubuntu 22.04+ assumed):
git clone https://github.com/<you>/openpartner
cd openpartner
cp .env.example .env
# Edit .env — set:
# OPENPARTNER_DOMAIN, CADDY_EMAIL, ADMIN_API_KEY,
# PORTAL_HOST, API_HOST, ROUTER_HOST, etc.
docker compose -f docker-compose.prod.yml up -dEvery push to main publishes three images to GitHub Container Registry:
ghcr.io/getcoherence/openpartner-api:edgeghcr.io/getcoherence/openpartner-router:edgeghcr.io/getcoherence/openpartner-portal:edge
Tagged releases (v1.2.3) publish 1.2.3, 1.2, 1, and latest. To run
those instead of building locally, swap each service's build: block in
docker-compose.prod.yml for image: ghcr.io/getcoherence/openpartner-<service>:edge
and docker compose pull before up -d. Fork users: replace getcoherence
with your GitHub username / org.
Caddy terminates TLS via Let's Encrypt automatically and reverse-proxies three subdomains (portal., api., go.) at the three services. Point their DNS at the box's IP and Caddy handles the rest.
# Follow the API's first-boot migrations:
docker compose -f docker-compose.prod.yml logs -f apigit pull
docker compose -f docker-compose.prod.yml up -d --buildMigrations run again on the API's next boot — zero-downtime per-service restart as each container cycles.
Point a cron at pg_dump against the Postgres service, or mount the postgres-data volume on block storage you back up. For anything bigger than a hobby deploy, use DO Managed Postgres (or any managed offering) instead of the bundled container.
Whichever host you pick:
| Subdomain | What it serves | Example |
|---|---|---|
| Portal | Partner + admin UI | partners.yourdomain.com |
| API | JSON HTTP API — webhooks, SDK, portal fetches | api.yourdomain.com |
| Router | Partner share links resolve here | go.yourdomain.com |
The portal's fetches go to /api/* relatively; App Platform's ingress rules and the compose's Caddyfile both route that to the API. You don't configure the portal's API URL separately.
The router URL is what creators embed in their promo content (go.yourdomain.com/r/graciefindsdeals), so pick something short and branded.
On first successful boot, sign in at https://partners.yourdomain.com/login with the API key tab, paste your ADMIN_API_KEY. From there:
- Create a partner (Admin → Partners → New partner).
- Create a campaign with a commission rule.
- Create a link for the partner.
- Embed the SDK in your product — see
packages/sdk/README.md. - Optional: join the Network (Admin → Network → Vendors) to start getting creator applications.