Skip to content
opportunora.
  • The loop
  • Product
  • Pricing
  • FAQ
  • Docs
  • Blog
Sign in Start free trial

Documentation · Reference

Run your own deployment

For whoever hosts Opportunora itself. The three processes, first install, the environment variables that matter, workers, Docker, backups, key rotation and the admin tools.

Updated 11 September 2026

All pages
  1. Start here

    1. What it is
    2. Create your workspace
    3. Your first hour
    4. The dashboard
  2. The loop

    1. Find leads
    2. Companies and deals
    3. Generate a website
    4. Edit a website
    5. Publish a website
    6. Sites that need a server
    7. Invoices
    8. Track time
    9. Get paid
  3. Connections

    1. Connect Cloudflare
    2. Connect your own domain
    3. Connect Stripe
    4. Connect PayPal
  4. Your workspace

    1. Settings
    2. AI credits and plans
    3. People and roles
    4. Prompt Studio
  5. Reference

    1. API and webhooks
    2. Run your own deployment
      1. What runs
      2. First install
      3. The environment
      4. AI providers
      5. Sign-in providers
      6. Payments
      7. Workers
      8. Docker
      9. Backups
      10. Rotating the encryption key
      11. Admin tools
      12. Run logs and cost
    3. Troubleshooting

Most people use a hosted workspace and never read this page. It is for whoever runs the app itself: the web server, the worker, the database, and the environment they read. Everything in the rest of the documentation assumes this part is done.

What runs

Piece Command Does
Postgres 16 npm run db:up, or any Postgres Holds everything, including every generated site's HTML.
The web app npm run build and npm start Serves the screens, the API and the pay pages.
At least one worker npm run worker Runs discovery, generation, publishing and the payments sweep.

The web server never does the slow work. A click writes a queued job and returns; a worker claims it. Without a worker, nothing fails — it just never happens. Discovery runs sit queued, generations sit at Starting — handing the job to a worker, publishes never produce an address, and invoices never turn overdue.

First install

npm install
cp .env.example .env.local   # then fill it in, see below
npm run db:up                # Postgres in Docker
npm run db:migrate
npm run dev                  # http://localhost:3000
npm run worker               # a second terminal

npm run db:seed loads a demo workspace and prints its sign-in. It is development data: it refuses to run against a non-local database unless you force it, and a real deployment should never carry it.

Warning

npm run dev, npm run dev:webpack and npm run build write incompatible output into the same .next directory. If the dev server starts answering every page with a 500 after a build, stop it, delete .next, and start it again — or set NEXT_DIST_DIR to build somewhere else.

The environment

.env.example documents every variable. The ones that decide whether the product works at all:

Variable Needed for Without it
DATABASE_URL Everything Nothing starts.
APP_URL Sign-in redirects, pay links, OAuth callbacks, PayPal webhooks Production refuses to boot. No trailing slash; it must match what you registered at Google, Microsoft and Stripe exactly.
AUTH_SECRET Google and Microsoft sign-in The provider buttons fail with Sign-in is not configured on this site.
CREDENTIAL_ENCRYPTION_KEY Connecting Cloudflare, PayPal Settings refuses to store either: Set an encryption key first.
An AI provider key Real discovery and generation Discovery returns seeded demo leads and generation uses a fixed template, silently.

Generate the two secrets with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Keep CREDENTIAL_ENCRYPTION_KEY out of the database and out of database backups. The split is what stops a leaked dump from being enough to publish to, and bill, someone else's Cloudflare account.

AI providers

Set one of ANTHROPIC_API_KEY, OPENAI_API_KEY or OPENROUTER_API_KEY. The vendor and model are infrastructure choices, set in the environment rather than in the app: DISCOVERY_PROVIDER and DISCOVERY_MODEL for research, GENERATION_PROVIDER and GENERATION_MODEL for building and editing sites, which fall back to the discovery ones.

Discovery must be able to search the web. Anthropic and OpenRouter search on their side; an OpenAI-compatible provider needs SEARXNG_URL, BRAVE_SEARCH_API_KEY or SERPER_API_KEY. A provider that cannot search counts as unconfigured, and runs fall back to the demo leads.

Caution

Provider keys belong to the deployment, not to a workspace. Sign-up is open, so every stranger who signs up spends your vendor balance, metered only by their credit allowance. A trial is about ten dollars of model cost at worst. Decide who can reach the sign-up page before you point a public domain at it.

Sign-in providers

Google needs GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET; Microsoft needs MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET and optionally MICROSOFT_TENANT_ID. Leave either pair incomplete and that button does not render. Register the callback as <APP_URL>/api/auth/callback/google or …/microsoft.

With MICROSOFT_TENANT_ID left at common, any Microsoft account can create a workspace. Set it to your directory's id to keep sign-in inside one company.

Payments

PAYMENTS_LIVEMODE=true for real money; anything else is sandbox. Stripe needs the platform setup described in Connect Stripe. PayPal needs no variable of its own, only the encryption key and an HTTPS APP_URL for its webhook.

Workers

Each worker process drains four queues by default — discovery, generation, publish and payments — and holds no state between jobs, so replicas are interchangeable.

  • More throughput: start more workers. DISCOVERY_CONCURRENCY, GENERATION_CONCURRENCY and PUBLISH_CONCURRENCY set how many jobs each runs at once.
  • Split the workloads: QUEUES=discovery on one machine, QUEUES=generation,publish on another.
  • Autoscale on running plus runnable jobs per queue from GET /api/queues/depth, enabled by QUEUE_METRICS_TOKEN. Runnable excludes jobs waiting for a workspace plan slot. Keep at least one worker for payment reconciliation and housekeeping.

Plans cap running discovery, generation and publishing jobs together: Trial/Solo 1, Studio 3, Agency 10 per workspace. Waiting jobs do not use a running slot. One generation per website and one publish per workspace prevent conflicting changes.

For Railway, the app repository includes railway/worker.json, railway/autoscaler.json, and docs/RAILWAY_SCALING.md with the setup instructions. The controller grows the worker service from queue demand and returns to its minimum after an idle period.

Run a worker from the repository root; it loads .env.local from where it starts.

Docker

The Dockerfile builds one image for both roles: npm start for the web app, npm run worker for workers. docker-compose.yml runs Postgres and, behind a profile, workers:

docker compose --profile worker up -d --build --scale worker=3

Compose does not start the web app; run the image with npm start behind your own proxy. For multiple replicas, set the same UPLOADS_S3_* bucket variables on web and workers. The private bucket stores logos, attachments and generated images for all instances. On a single host, put UPLOADS_DIR (default .uploads) on shared persistent storage. Railway volumes do not support replicas; use a Railway Bucket there.

Backups

There is no export in the app. Back up three things, separately:

  1. The database — every workspace, every invoice, and every generated site's HTML.
  2. The uploads directory — logos, attachments, and the pictures copied from clients' old sites. They are not in the database.
  3. CREDENTIAL_ENCRYPTION_KEY — somewhere other than the database backup. Without it no stored Cloudflare token or PayPal secret can be opened.

Rotating the encryption key

  1. Generate a new key.
  2. Move the old one to CREDENTIAL_ENCRYPTION_KEY_PREVIOUS, put the new one in CREDENTIAL_ENCRYPTION_KEY, and restart every web and worker process.
  3. npm run secrets:reseal -- --check lists anything neither key opens.
  4. npm run secrets:reseal rewrites every stored credential under the new key.
  5. Remove CREDENTIAL_ENCRYPTION_KEY_PREVIOUS and restart.

A value neither key opens stays as it is; that workspace reconnects the account.

Admin tools

There is no admin screen. Plans, credits and announcements are command-line tools:

npm run credits -- <workspace> show
npm run credits -- <workspace> grant 500 --kind topup --note "Goodwill after the outage"
npm run credits -- <workspace> plan studio
npm run credits -- <workspace> ledger --limit 20

<workspace> is the workspace id or its slug. --note is required on a grant.

Note

credits … plan moves the credit allowance only. A workspace still on its trial keeps being treated as a trial — and stops when the trial ends — until its subscription record is moved too.

To post a message to everyone's notification bell:

node scripts/announce.js maintenance-0912 "Scheduled maintenance tonight at 22:00" --preview
node scripts/announce.js maintenance-0912 "Scheduled maintenance tonight at 22:00"

The first argument is a key you choose. Sending the same key twice only reaches people who do not have it yet. --to limits it to one workspace, --href sets where the bell links, and --preview reports who would get it without sending.

Run it with node, not npm run: npm swallows the flags, and a rehearsal with --preview turns into a send.

Run logs and cost

Every finished discovery run writes a JSON file to logs/discovery-runs/ and a line to index.ndjson beside it: what it found, what it cost in tokens and searches, and what that cost in money at the prices configured. npm run discovery:cost summarises them. It is the data you price the product from.

Previous API and webhooks Next Troubleshooting

Did this leave something out?

Write to info@opportunora.com and say which page you were on. The documentation is part of the product, so a gap in it is a bug.

opportunora.

Find local businesses. Build their website. Publish. Invoice. Repeat.

The page

  • The loop
  • Product
  • Docs
  • Blog

Get started

  • Pricing
  • FAQ
  • Start free trial
  • Sign in

Legal

  • Privacy
  • Terms
© 2026 opportunora

Allow analytics cookies?

Google Analytics shows us how people use this page. Choose Only essentials and it never loads; the page then stores nothing but your choice.