Last updated: 2026-03-20.
The app runs on Railway with three services from a single codebase:
| Service | Type | Start command |
|---|---|---|
| web | Next.js server | npx drizzle-kit push && npm start |
| worker | graphile-worker process | npx tsx src/worker/index.ts |
| Postgres-JwGd | Managed PostgreSQL | (managed by Railway) |
The web service runs drizzle-kit push on every deploy to apply any pending schema changes before starting Next.js. The worker process runs background jobs (campaign sending, script execution, churn detection).
- Project:
pauseai-crm - Project ID:
d4b10f4b-d227-4a7a-a8a8-c3e8421183f9 - URL: https://web-production-4523c.up.railway.app
- Dashboard: https://railway.com/project/d4b10f4b-d227-4a7a-a8a8-c3e8421183f9
| Service | ID |
|---|---|
| web | 8875b979-e135-4264-b4ee-008df6089335 |
| worker | 1aca9bf6-97bb-4bfb-9f31-95fa9c666eab |
| Postgres-JwGd | bf86975f-4c53-4299-b386-3827fc3f1254 |
Both services deploy from local files using railway up. Because railway.toml defines the start command, you need to swap it per service.
# 1. Set railway.toml for web
cat > railway.toml << 'EOF'
[build]
builder = "RAILPACK"
[deploy]
startCommand = "npx drizzle-kit push && npm start"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
EOF
# 2. Deploy
railway up --detach --service web# 1. Set railway.toml for worker
cat > railway.toml << 'EOF'
[build]
builder = "RAILPACK"
[deploy]
startCommand = "npx tsx src/worker/index.ts"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
EOF
# 2. Deploy
railway up --detach --service worker# Deploy web
cat > railway.toml << 'EOF'
[build]
builder = "RAILPACK"
[deploy]
startCommand = "npx drizzle-kit push && npm start"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
EOF
railway up --detach --service web
# Deploy worker
cat > railway.toml << 'EOF'
[build]
builder = "RAILPACK"
[deploy]
startCommand = "npx tsx src/worker/index.ts"
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
EOF
railway up --detach --service workerTip: Some credentials (MailerSend API key and from-email) can be managed directly in the app at Settings → Integrations without touching env vars or redeploying. DB-stored values take precedence over env vars.
| Variable | Description | Required |
|---|---|---|
DATABASE_URL |
PostgreSQL connection (uses Railway reference: ${{Postgres-JwGd.DATABASE_URL}}) |
Yes |
NEXTAUTH_SECRET |
Cryptographic secret for JWT sessions | Yes |
NEXTAUTH_URL |
Public URL of the web service | Yes |
AUTH_GOOGLE_ID |
Google OAuth client ID | Yes |
AUTH_GOOGLE_SECRET |
Google OAuth client secret | Yes |
MAILERSEND_API_KEY |
Mailersend API key fallback (prefer setting via Settings → Integrations in the UI) | No |
MAILERSEND_FROM_EMAIL |
From address fallback (prefer setting via Settings → Integrations in the UI) | No |
ADMIN_EMAILS |
Comma-separated emails auto-promoted to admin on sign-in | Recommended |
UNSUBSCRIBE_SECRET |
HMAC secret for unsubscribe tokens (openssl rand -hex 32) |
Yes |
NEXT_PUBLIC_APP_URL |
Public URL used in unsubscribe links | Yes |
EMAIL_ENCRYPTION_KEY |
AES-256 key for encrypting OAuth tokens and connection credentials (openssl rand -hex 32) |
Yes |
MAILERSEND_WEBHOOK_SIGNING_SECRET |
HMAC secret for verifying Mailersend webhook signatures (from Mailersend dashboard) | Yes (if using Mailersend) |
TALLY_WEBHOOK_SIGNING_SECRET |
HMAC secret for verifying Tally webhook signatures (from Tally form settings) | Yes (if using Tally forms) |
EMAIL_MODE |
live for production (sends real email via Mailersend). Defaults to sandbox if unset — never set to sandbox in production |
Yes |
NODE_ENV |
Must be production |
Yes |
DEV_BYPASS_AUTH |
Must be false in production (bypass only works when NODE_ENV=development) |
No |
| Variable | Description | Required |
|---|---|---|
DATABASE_URL |
PostgreSQL connection (same as web) | Yes |
MAILERSEND_API_KEY |
Mailersend API key fallback (prefer setting via the UI) | No |
MAILERSEND_FROM_EMAIL |
From address fallback (prefer setting via the UI) | No |
UNSUBSCRIBE_SECRET |
Same value as web — needed for unsubscribe URL generation during campaign sends | Yes |
NEXT_PUBLIC_APP_URL |
Same value as web — used in unsubscribe URLs | Yes |
EMAIL_ENCRYPTION_KEY |
Same value as web — needed for credential decryption during sync | Yes |
EMAIL_MODE |
live — same value as web. Worker sends campaign emails, so it must also be in live mode |
Yes |
NODE_ENV |
production |
Yes |
railway service web
railway variables set KEY=value
railway service worker
railway variables set KEY=value# Web logs
railway service web && railway logs -n 50
# Worker logs
railway service worker && railway logs -n 50- Users sign in via Google OAuth at
/login - Sessions use JWT strategy (stateless, no session table lookups)
- Role stored in the
usertable (rolecolumn:admin,member,viewer) — this is the global role. Per-workspace roles are inuser_workspaces.
Emails listed in the ADMIN_EMAILS environment variable are automatically promoted to admin on their first sign-in. Currently configured: maxime@pauseai.fr.
This development convenience flag only activates when both conditions are met:
NODE_ENV === "development"DEV_BYPASS_AUTH === "true"
In production (NODE_ENV=production), the bypass is always inactive regardless of the DEV_BYPASS_AUTH value. It is still good practice to set it to false or remove it in production.
All OAuth and Gmail API credentials live in the pauseai-everything GCP project. The OAuth consent screen is set to External (users with any email domain can connect). While in "Testing" mode, only manually added test users can authenticate — submit for verification when ready for general access.
A single OAuth client is used for both login and Gmail integration. The following redirect URIs and origins must be configured in Google Cloud Console:
Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google(login — dev)http://localhost:3000/api/auth/gmail/callback(Gmail — dev)https://web-production-4523c.up.railway.app/api/auth/callback/google(login — prod)https://web-production-4523c.up.railway.app/api/auth/gmail/callback(Gmail — prod)
Authorized JavaScript origins:
http://localhost:3000https://web-production-4523c.up.railway.app
The Gmail API and the gmail.readonly scope must be enabled in the project.
| Task | Trigger | Description |
|---|---|---|
send_campaign |
On-demand (enqueued by API) | Sends a campaign to all contacts in its segment |
run_script |
On-demand or scheduled | Executes a user-defined script in a sandboxed VM |
dispatch_scripts |
Cron: every minute (* * * * *) |
Checks for enabled scripts with cron schedules and enqueues run_script jobs |
detect_churn |
Cron: daily at 6am UTC (0 6 * * *) |
Flags dormant contacts based on inactivity |
sync_email_interactions |
On-demand (enqueued by dispatcher or manual refresh) | Fetches Gmail messages, matches to CRM contacts, creates interactions |
dispatch_email_syncs |
Cron: every minute (* * * * *) |
Enqueues email connections whose sync interval has elapsed |
The scripts table doesn't exist yet. Redeploy the web service — drizzle-kit push in its start command will create missing tables.
Run a web deploy to push schema changes: the web start command runs drizzle-kit push automatically.
Make sure NEXTAUTH_URL matches the actual public domain and that the redirect URI is registered in Google Cloud Console.
The campaign was queued but the worker isn't running or failed mid-send. Check worker logs. The worker can be redeployed to restart processing.