Configuration map
Runtime, authentication, media, 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.
Media storage and delivery
| Variable | Required | Purpose |
|---|---|---|
PROSEWIRE_MEDIA_ENDPOINT | Cloud; optional self-hosted | S3-compatible API endpoint |
PROSEWIRE_MEDIA_REGION | No | Object-storage region; defaults to auto |
PROSEWIRE_MEDIA_BUCKET | With endpoint | Private bucket for uploads and processed variants |
PROSEWIRE_MEDIA_ACCESS_KEY_ID | With endpoint | Object-storage access-key identifier |
PROSEWIRE_MEDIA_SECRET_ACCESS_KEY | With endpoint | Object-storage secret key |
PROSEWIRE_MEDIA_DELIVERY | No | cdn (default) or signed for a private bucket without a CDN |
PROSEWIRE_MEDIA_PUBLIC_URL | CDN mode | CDN origin used for processed asset URLs; unset in signed mode |
PROSEWIRE_MEDIA_FORCE_PATH_STYLE | No | Enables path-style bucket addressing; defaults to false |
PROSEWIRE_MEDIA_MAX_UPLOAD_BYTES | No | Maximum reservation size from 1 byte through 100 MiB; defaults to 20 MiB |
PROSEWIRE_MEDIA_UPLOAD_URL_EXPIRES_SECONDS | No | Signed upload lifetime from 60 through 3600 seconds; defaults to 600 |
Set the endpoint, bucket and both credentials together. CDN mode also requires the media public URL. Signed mode requires an explicit PROSEWIRE_PUBLIC_URL and rejects PROSEWIRE_MEDIA_PUBLIC_URL. Cloud mode rejects a missing media configuration. Self-hosted mode can omit it and continue using external cover-image URLs.
The media bucket must stay private. Put a CDN in front of it and configure the CDN to serve only processed keys under publications/. Deny the temporary _uploads/ prefix. Processed filenames contain a checksum, so the CDN may cache them; Prosewire records the resulting CDN URL with each variant.
For a private Railway bucket without a CDN, set PROSEWIRE_MEDIA_DELIVERY=signed. Prosewire records stable /media/... URLs and redirects authorized requests to one-minute signed bucket URLs. The application transfers no image bytes. Public requests require a ready variant referenced by a published, non-future post in the same publication; authenticated workspace members can preview unpublished variants. Raw uploads, failed or deleted assets, and unknown keys return 404. Imported image references in published post bodies also work. Management keys do not grant image preview access through this route.
Redirects use Cache-Control: no-store. An already issued bucket URL remains usable until its one-minute expiry, including after archiving a post or revoking a member. Treat these temporary URLs as credentials and do not log them. Existing CDN URLs are unchanged when switching modes; migrate existing variant and cover URLs separately before switching a populated installation.
Browser uploads use a signed PUT request. The bucket CORS policy must allow the exact Prosewire dashboard origin, the PUT method, and the Content-Type request header. Do not use a wildcard origin for a credentialed production dashboard. The application checks the reserved byte count against the object metadata, reads the object back, detects its MIME type from the image, rejects animated or multi-page inputs, and produces sanitized original, 1600-pixel, and 480-pixel variants.
Each publication starts with a 1 GiB storage quota recorded in Postgres. Pending reservations and ready variants count toward the quota. Operators can change blog.media_storage_quota_bytes through a controlled database operation until a quota administration surface is added.
Media storage credentials must allow reading, writing, deleting, and listing objects in the configured bucket. Listing the publications/ prefix lets recovery remove variants left by a terminated web process. Processing times out after five minutes; if a process is killed before cleanup, retry completion or remove the asset after its 15-minute processing lease expires. Both operations release its reserved quota and remove abandoned variants. Storage cleanup failures are logged and removal can be retried.
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 | For email delivery | SMTP or SMTPS connection URL; missing or blank disables delivery without blocking worker startup |
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.
Without SMTP_URL, the worker logs a warning and leaves pending emails in the outbox or queue while scheduled publishing and retention continue. Configure SMTP and restart the worker to resume delivery. 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 runs awaited scans for scheduled publishing, raw-view retention, and outbox draining, and starts durable workflows for email delivery. Postgres LISTEN/NOTIFY normally drains the outbox immediately. Scans log failures and retry on the next cycle; retention also retries three times at one-minute intervals. Empty scans create no persisted workflow execution. 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. Transient SMTP failures use exponential retry with a thirty-second cap and finish as a typed workflow failure after six attempts. Permanent recipient, authentication, and configuration failures stop immediately. A terminal error log includes the outbox ID; after correcting the cause, issue a new invitation to create a fresh delivery intent. SMTP DNS, connection, and greeting timeouts are ten seconds; socket inactivity timeout is thirty seconds. Shutdown waits for an active send to settle before releasing its queue lease and closing the transport. These are per-phase and inactivity limits, not an absolute shutdown deadline: an SMTP peer that keeps making progress can extend the send. SMTP delivery remains at-least-once when a process dies after the server accepts a message.
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, Redis, and the media buckets, then test a coordinated restore before launch
Email execution retention
The worker checks email executions daily. Once a dispatched intent is older than thirty days and its workflow has a terminal result, it clears that workflow’s persisted messages/replies and scrubs the recipient, subject, body, and error from the outbox row. The outbox ID and dispatch marker remain as a deduplication record. Running, suspended, unknown, and recent executions are preserved. Cleanup also removes the matching Redis queue identity. It preserves queued, pending, locked, and failed queue identities. Legacy identities defer cleanup while the queue has waiting work. Cleanup records its intent before clearing workflow storage, so a restart can finish cleanup even when the workflow result has already been removed.
Migration 0016_cold_darkhawk adds two nullable cleanup timestamps without
rewriting existing payloads. Run the normal one-shot migration before starting
the new worker. Back up PostgreSQL before upgrading; rolling back the worker
requires no schema rollback because old workers ignore the new columns. To
recover historical delivery payloads after retention, restore the matching
PostgreSQL backup to a separate database for inspection. Do not clear dispatch
markers or restore only workflow tables into a live worker database, because
that can replay previously dispatched email.
Effect RC.115 stores queue identities in Redis sorted sets. At startup the worker converts legacy sets in restartable batches, retaining the original set until the final atomic replacement. Stop the old worker and back up Redis before upgrading; run exactly one worker during conversion. Pending payloads and locks are preserved. To roll back to RC.112, stop the worker and restore the matching Redis and PostgreSQL backups; an old worker cannot read the new sorted sets.