Configuration map
Runtime, authentication, email, bootstrap, worker, and documentation-site settings for Prosewire.
Keep production configuration in the deployment platform’s secret store. Do not commit a populated .env file or bake secrets into an image.
Runtime and database
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | Yes | Postgres connection used by migrations, web, and worker |
REDIS_URL | Worker | Redis connection used by Effect DurableQueue for email delivery |
PROSEWIRE_PUBLIC_URL | Production | External origin used by the server for links such as invitations |
NEXT_PUBLIC_PROSEWIRE_PUBLIC_URL | Production | Matching browser-visible origin used by the authentication client |
PROSEWIRE_DEPLOYMENT | No | self-hosted for one implicit team or cloud for multiple explicit workspaces; defaults to self-hosted |
PROSEWIRE_DEFAULT_BLOG | No | Development default publication slug; defaults to fieldnotes |
PROSEWIRE_MIGRATIONS_DIR | Container default | Override the directory containing committed Drizzle migrations |
POSTGRES_PASSWORD | Default Compose only | Password interpolated into the bundled Postgres service and application URL |
Use the same DATABASE_URL for the migration, web, and worker processes. The worker reserves one Postgres connection for email outbox notifications and opens a small Effect SQL pool for workflow storage. Run exactly one worker process for each database. The web process writes email intents to Postgres and does not connect to Redis. In Compose, choose a URL-safe POSTGRES_PASSWORD because it is interpolated into a connection URL.
The Redis account must permit SCRIPT LOAD, EVALSHA, publish, subscribe, list, set, hash, string, and expiry commands. Use AOF persistence, replication, backups, and maxmemory-policy noeviction in production. Postgres retains the original email intent and the workflow result. Redis owns the queued delivery payload while the workflow waits. Back up both systems because losing Redis after Postgres marks an intent dispatched can leave an email workflow waiting for a queue item that no longer exists.
The two public URL values should be the same external HTTPS origin seen by users. Do not include a path suffix.
Authentication and email
| Variable | Required | Purpose |
|---|---|---|
BETTER_AUTH_SECRET | Yes | Unique authentication secret of at least 32 characters |
PROSEWIRE_ALLOW_SIGN_UP | No | Enables first-owner bootstrap for self-hosted or open registration for Cloud; defaults to false |
SMTP_URL | Production invitations | SMTP or SMTPS connection URL |
EMAIL_FROM | Production email | Sender identity; defaults to a localhost address in development |
On self-hosted deployments, enable PROSEWIRE_ALLOW_SIGN_UP=true only to create the first owner. Direct registration closes automatically after the first account exists, even before that owner creates the team, but you should still set the value back to false and replace the web container after bootstrap. On Cloud, the flag keeps open registration available until you disable it. A valid, unexpired invitation still allows its invited email address to create an account in either deployment mode.
Test SMTP delivery and invitation links from the public origin before onboarding a team.
Self-hosted administrator bootstrap
| Variable | Required | Purpose |
|---|---|---|
PROSEWIRE_BOOTSTRAP_ADMIN_EMAIL | With bootstrap password | Email for the first self-hosted administrator |
PROSEWIRE_BOOTSTRAP_ADMIN_PASSWORD | With bootstrap email | Temporary password between 12 and 128 characters |
PROSEWIRE_BOOTSTRAP_ADMIN_NAME | No | Display name; defaults to Prosewire Admin |
These variables need no separate enable flag. When both credentials exist, the one-shot migration job creates an administrator only if the installation has no users and no workspace. It marks that account for a required password change. The dashboard and authenticated mutations remain unavailable until the administrator replaces the temporary password. The change revokes every session, so the administrator signs in again before creating the first publication.
Remove the variables after the password change. Later migration runs do not reset a completed bootstrap account. Until the password changes, an operator can replace the temporary password value and rerun the migration job to recover access. This recovery works only for the sole matching bootstrap administrator before a workspace exists. Cloud deployments reject bootstrap administrator credentials.
Rolling and multi-instance deployments
| Variable | Required | Purpose |
|---|---|---|
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY | Multi-instance production | Stable base64-encoded 32-byte key shared by web replicas and releases |
NEXT_DEPLOYMENT_ID | Web build | Release identifier baked into the web assets and shared by replicas running that image |
Generate the server-action key with openssl rand -base64 32 and store it as a secret. Rotating it during a rollout can break requests created by a different replica or release.
Pass NEXT_DEPLOYMENT_ID to next build or as the bundled Docker build argument. Use an immutable version or commit, and run the same built image on every web replica for that release. Changing the variable on a running container does not rewrite its assets.
Worker configuration
| Variable | Required | Purpose |
|---|---|---|
PROSEWIRE_ANALYTICS_RETENTION_DAYS | No | Positive integer controlling raw page-view retention; defaults to 365 |
PROSEWIRE_EMAIL_WORKER_CONCURRENCY | No | Concurrent email deliveries inside the single worker; defaults to 4 |
The worker starts durable workflows for scheduled publishing, raw-view retention, outbox draining, and email delivery. Postgres LISTEN/NOTIFY normally starts the outbox workflow immediately. A 30-second scan catches notifications missed while the listener reconnects, and expired leases resume safely after a restart. The email workflow waits for an Effect DurableQueue worker in Redis. Delivery failures use exponential retry with a five-minute cap and finish as a typed workflow failure after 100 attempts.
The pinned Effect SQL runner supports durable single-process execution. Do not scale the worker command above one replica for the same database. Increase PROSEWIRE_EMAIL_WORKER_CONCURRENCY if SMTP capacity allows more parallel delivery. The migration role and worker database role need access to the Effect-owned cluster_* tables. The migration command creates them first, and worker startup repeats Effect’s idempotent migration check.
Development seed only
| Variable | Required in development | Purpose |
|---|---|---|
ADMIN_EMAIL | Yes | Email for the local seeded administrator |
ADMIN_PASSWORD | Yes | Unique password of at least 12 characters |
PROSEWIRE_SEED_API_KEY | No | One-time local key of at least 24 characters with read and write scopes |
The example values are local-only. Development web startup runs migrations and seed logic; production web startup does neither. Production uses the one-shot migration command and the optional self-hosted administrator bootstrap instead.
Development and test controls
PROSEWIRE_ALLOWED_DEV_ORIGINS is a comma-separated Next.js development-origin allowlist. PROSEWIRE_EXPOSE_TESTING_API=1 exposes framework testing support in production builds and is reserved for controlled acceptance tests; never enable it on a public deployment.
Documentation site
The separate Astro landing page and documentation site uses SITE_URL for canonical links, its sitemap, and robots.txt:
SITE_URL=https://docs.example.com pnpm --filter @prosewire/site build:productionOn Vercel, VERCEL_PROJECT_PRODUCTION_URL supplies the origin when system environment variables are enabled. SITE_URL overrides it for manual or non-Vercel builds. A production build rejects a missing, local, reserved, or known dead origin. These variables configure the documentation site only; they do not configure the Prosewire publishing application.
Security checks
- Use unique values per environment
- Keep Postgres on a private network and require TLS for remote connections
- Store API tokens as hashes and use the narrowest read/write scope
- Rotate a compromised API key by creating a replacement, changing the consumer, then revoking the old key
- Back up Postgres and test a restore before launch