ProsewireDocs

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

VariableRequiredPurpose
DATABASE_URLYesPostgres connection used by migrations, web, and worker
REDIS_URLWorkerRedis connection used by Effect DurableQueue for email delivery
PROSEWIRE_PUBLIC_URLProductionExternal origin used by the server for links such as invitations
NEXT_PUBLIC_PROSEWIRE_PUBLIC_URLProductionMatching browser-visible origin used by the authentication client
PROSEWIRE_DEPLOYMENTNoself-hosted for one implicit team or cloud for multiple explicit workspaces; defaults to self-hosted
PROSEWIRE_DEFAULT_BLOGNoDevelopment default publication slug; defaults to fieldnotes
PROSEWIRE_MIGRATIONS_DIRContainer defaultOverride the directory containing committed Drizzle migrations
POSTGRES_PASSWORDDefault Compose onlyPassword 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

VariableRequiredPurpose
PROSEWIRE_MEDIA_ENDPOINTCloud; optional self-hostedS3-compatible API endpoint
PROSEWIRE_MEDIA_REGIONNoObject-storage region; defaults to auto
PROSEWIRE_MEDIA_BUCKETWith endpointPrivate bucket for uploads and processed variants
PROSEWIRE_MEDIA_ACCESS_KEY_IDWith endpointObject-storage access-key identifier
PROSEWIRE_MEDIA_SECRET_ACCESS_KEYWith endpointObject-storage secret key
PROSEWIRE_MEDIA_DELIVERYNocdn (default) or signed for a private bucket without a CDN
PROSEWIRE_MEDIA_PUBLIC_URLCDN modeCDN origin used for processed asset URLs; unset in signed mode
PROSEWIRE_MEDIA_FORCE_PATH_STYLENoEnables path-style bucket addressing; defaults to false
PROSEWIRE_MEDIA_MAX_UPLOAD_BYTESNoMaximum reservation size from 1 byte through 100 MiB; defaults to 20 MiB
PROSEWIRE_MEDIA_UPLOAD_URL_EXPIRES_SECONDSNoSigned 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

VariableRequiredPurpose
BETTER_AUTH_SECRETYesUnique authentication secret of at least 32 characters
PROSEWIRE_ALLOW_SIGN_UPNoEnables first-owner bootstrap for self-hosted or open registration for Cloud; defaults to false
SMTP_URLFor email deliverySMTP or SMTPS connection URL; missing or blank disables delivery without blocking worker startup
EMAIL_FROMProduction emailSender 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

VariableRequiredPurpose
PROSEWIRE_BOOTSTRAP_ADMIN_EMAILWith bootstrap passwordEmail for the first self-hosted administrator
PROSEWIRE_BOOTSTRAP_ADMIN_PASSWORDWith bootstrap emailTemporary password between 12 and 128 characters
PROSEWIRE_BOOTSTRAP_ADMIN_NAMENoDisplay 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

VariableRequiredPurpose
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYMulti-instance productionStable base64-encoded 32-byte key shared by web replicas and releases
NEXT_DEPLOYMENT_IDWeb buildRelease 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

VariableRequiredPurpose
PROSEWIRE_ANALYTICS_RETENTION_DAYSNoPositive integer controlling raw page-view retention; defaults to 365
PROSEWIRE_EMAIL_WORKER_CONCURRENCYNoConcurrent 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

VariableRequired in developmentPurpose
ADMIN_EMAILYesEmail for the local seeded administrator
ADMIN_PASSWORDYesUnique password of at least 12 characters
PROSEWIRE_SEED_API_KEYNoOne-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:production

On 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.