Deploy to DO App Platform
The repo includes .do/app.yaml, an App Platform spec that deploys OpenPartner as three
components of one app. You bring the Postgres database.
What gets deployed
| Component | Kind | Port | Serves |
|---|---|---|---|
api | Service, built from apps/api/Dockerfile | 4601 | /api/*. Runs database migrations on start. |
router | Service, built from apps/router/Dockerfile | 4701 | /r/*: partner links and the _cref cookie |
portal | Static site on App Platform’s CDN | Everything else: the admin and partner UI |
The spec’s ingress rules send /api/* to the API (with the /api prefix stripped) and /r/* to
the router, so one domain serves the whole app.
The spec doesn’t create a database. Use a DigitalOcean Managed Postgres cluster, or any
Postgres the app can reach, and give its connection string to both services as DATABASE_URL.
To have App Platform create a database with the app instead, uncomment the databases block at
the top of the spec and set DATABASE_URL on both services to ${openpartner-db.DATABASE_URL}.
Before you create the app
Fork getcoherence/openpartner to your GitHub, then edit .do/app.yaml in your fork:
-
Point it at your fork. All three components build from
github.repo: getcoherence/openpartner. Change each one to your fork, so App Platform builds and redeploys from it. -
Switch the API to self-host settings. The spec is set up the way OpenPartner’s hosted service runs it, with
OPENPARTNER_MODE: flatandOPENPARTNER_TENANCY: multi. For your own install, change both on theapicomponent:- key: OPENPARTNER_MODEvalue: selfhost- key: OPENPARTNER_TENANCYvalue: singleflatis a hosted billing mode. Among other things, it refuses the Stripe Connect payouts thatselfhostsends from your own Stripe balance. -
Delete
DATABASE_URL_APPandOPENPARTNER_APP_DB_PASSWORDfrom theapicomponent. A single-tenant API can run asDATABASE_URL, but it only falls back to it whenDATABASE_URL_APPis unset, not when it’s empty.
To host several brands on one install instead (each under /t/<slug>/), keep
OPENPARTNER_TENANCY: multi and both database entries. The API refuses to start in multi
until they’re set (see below).
Create the app
-
In App Platform, Create App → GitHub and pick your fork. App Platform detects
.do/app.yaml. Or rundoctl apps create --spec .do/app.yaml. -
Fill in the encrypted secrets the spec declares.
On api:
DATABASE_URL: the Postgres connection string.SECRETS_ENCRYPTION_KEY: 32 bytes, hex or base64 (openssl rand -hex 32). It encrypts the mail credentials you save in the admin UI. The API won’t start without it.PORTAL_URL: the URL you’ll serve the portal on, e.g.https://partners.yourdomain.com. It’s the API’s CORS allowlist and the base of invite links. The API won’t start without it.ADMIN_API_KEY: the bootstrap admin key. Generate one withnode -e "console.log('op_' + require('crypto').randomBytes(24).toString('hex'))".METRICS_TOKEN: recommended./api/metricsis reachable from the internet, so set a token to require it as a bearer.MAIL_FROM,POSTMARK_SERVER_TOKEN: optional. You can set up mail in the install wizard instead. These are used only when the admin UI has no mail settings.STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET: optional. Your own Stripe account, to pay partners over Stripe Connect and take in Stripe revenue. Leave them empty to pay partners by hand.STRIPE_WEBHOOK_SECRETis the legacy combined secret; to tie each signing secret to its Stripe event destination, addSTRIPE_WEBHOOK_SECRET_PLATFORMandSTRIPE_WEBHOOK_SECRET_CONNECTas encrypted env vars instead (see Stripe).- Leave the other
STRIPE_*secrets empty. They’re for OpenPartner’s hosted plans. - With
OPENPARTNER_TENANCY: multionly:OPENPARTNER_APP_DB_PASSWORD: a random string of 32+ characters. On each start the API creates theopenpartner_appPostgres role with this password, or updates it.DATABASE_URL_APP: theDATABASE_URLconnection string withopenpartner_appand that password as the user and password. Postgres row-level security keeps brands apart only on this connection.
On router:
DATABASE_URL: the same connection string.COOKIE_DOMAIN: your site’s domain with a leading dot, e.g..yourdomain.com, so the_crefcookie reaches the pages where you run the SDK. Unset, the router sets it forlocalhost.
-
Click Create Resources.
-
If your database cluster limits connections to Trusted Sources, add the app to them.
The API runs migrations when it starts, so an empty database sets itself up. Once the deploy
finishes, open the app’s URL. With no admin yet, the portal sends you to the install wizard at
/install.
Scheduled jobs
The spec sets OPENPARTNER_ENABLE_SCHEDULER: '1', so the API approves commissions after their
holdback, pays partners on your payout cadence and runs housekeeping on its own. See
Scheduled jobs for what runs when, and set Payout
cadence to Manual first if you want to pay partners by hand.
Keep the API at one instance (instance_count: 1). Every instance of a component gets the same
env, so scaling it out would run the scheduler on each.
Custom domains
Add your domains in App Platform → Settings → Domains. DO handles certificate provisioning. A typical layout:
partners.yourdomain.comfor the portal and API. SetPORTAL_URLto it.go.yourdomain.comfor partner links, e.g.https://go.yourdomain.com/r/<linkKey>. Partners paste these into their content, so keep it short.
Partner links also work on the app’s default ondigitalocean.app URL, because the spec routes
/r/* to the router.
Keep domains in one place. doctl apps update --spec replaces the app’s whole spec,
domains included. If you add domains in the console, also add them to a domains block in your
fork’s spec, or make later changes in the console only.
File uploads (brand logos, creator avatars)
App Platform containers are ephemeral — local-disk storage gets wiped on every redeploy. For production, use DO Spaces (S3-compatible) so uploaded logos and avatars survive deploys.
-
Create a Spaces bucket in the same region as your app.
-
Generate a Spaces access key under API → Spaces Keys.
-
Add these env vars on the api component (encrypted):
OPENPARTNER_STORAGE_KIND=s3OPENPARTNER_STORAGE_S3_BUCKET=<bucket name>OPENPARTNER_STORAGE_S3_REGION=<bucket region, e.g. nyc3>OPENPARTNER_STORAGE_S3_ENDPOINT=https://<region>.digitaloceanspaces.comOPENPARTNER_STORAGE_S3_ACCESS_KEY_ID=<key>OPENPARTNER_STORAGE_S3_SECRET_ACCESS_KEY=<secret>If you’ve enabled the bucket’s CDN, also set
OPENPARTNER_STORAGE_S3_PUBLIC_BASE=https://<bucket>.<region>.cdn.digitaloceanspaces.com.
Files written to Spaces get public-read ACL with a 1-year immutable cache header. Object
keys are random hex (16 bytes), so make sure the bucket’s “Restrict File Listing” is on.
Updating
Push to your fork’s main branch. The spec sets deploy_on_push, so App Platform rebuilds and
redeploys, and the API runs any new migrations when it starts. To change the app’s settings
later, edit them in the console or run doctl apps update <APP_ID> --spec .do/app.yaml.