Deploy the hosted service: Vercel + Railway¶
The setup behind weirling.com: the web app on Vercel, the API, workers, Postgres and Redis on Railway, the domain and mailbox at Spaceship (Spacemail). Every step is something you click or paste; nothing is built on your machine.
weirling.com, www -> Vercel (Next.js, built from frontend/)
api.weirling.com -> Railway: api (FastAPI)
worker-ingest, worker-digest, beat (Celery)
Postgres + pgvector, Redis (private network only)
mail -> Spacemail (hello@weirling.com sends the digests)
Expected cost: Vercel Pro as you have it; Railway Hobby about $20 to $30 a month (about 2 GB of memory in use), including the $5 plan fee.
0. Before you start¶
-
Generate four secrets on your machine and keep them in a password manager:
-
A paid LLM key (Groq: the free tier runs out after about 40 summaries a day). Without one the app still works, with extract summaries.
- Recommended: cut the first release so you can pin a version. Edit the first
line of
docs/releases/v0.3.0.mdfrom(unreleased)to today's date and merge it; the Release workflow tagsv0.3.0and publishes the0.3.0images (see Upgrading). Until then you can use:latest.
1. Email: Spacemail on weirling.com¶
- In Spaceship, Spacemail, Connect domain: pick
weirling.com. Because the domain's DNS is at Spaceship, Spacemail adds its MX, SPF and DKIM records for you. -
Create four mailboxes, one per job:
Mailbox Job hello@The public contact address. support@Replies to any app email land here ( REPLY_TO_EMAIL), and the app shows it for help (SUPPORT_EMAIL).privacy@Data and takedown requests ( GRIEVANCE_CONTACT_EMAIL), shown on /contact. Keep it separate: these have legal deadlines and need a clean record.mohit@You. Don't forward them into one inbox: add all of them to one mail app (Apple Mail, Thunderbird, or the Spacemail app) so you see everything in one place and each reply goes out from the right address. 3. Add a DMARC record (Spaceship, Domains, weirling.com, DNS):
Type Host Value TXT _dmarcv=DMARC1; p=none; rua=mailto:hello@weirling.comStart with
p=none; after two clean weeks, change it top=quarantine. 4. Send a test email from webmail to a Gmail address; in Gmail, "Show original" should say SPF, DKIM and DMARC: PASS.
1.5 Sending app email: Resend¶
Railway's Hobby plan blocks outgoing SMTP (ports 25, 465 and 587), so the app sends through Resend's HTTPS API instead; Spacemail keeps the human mailboxes.
- In Resend, Domains, Add Domain:
mail.weirling.com(a subdomain keeps bulk-digest reputation away from your main domain). Pick the region nearest your Railway services; it can't change later. - Add the records Resend shows (DKIM TXT, and MX plus SPF TXT for the
send.mailreturn path) at Spaceship, then Verify. - API Keys, Create: permission Sending access, domain
mail.weirling.com. This isRESEND_API_KEY.
With RESEND_API_KEY set, EMAIL_PROVIDER=auto uses Resend. On Railway Pro
(SMTP allowed) you can use Spacemail's SMTP instead: set SMTP_HOST,
SMTP_PORT, SMTP_USER, SMTP_PASSWORD and leave RESEND_API_KEY empty.
Limits: Spacemail paid plans send up to 500 emails an hour per mailbox, plenty for the beta. Move digests to a transactional sender (Resend, Postmark) before that becomes tight.
2. Railway: the backend¶
Create a project named weirling. Add services in this order.
2.1 Postgres with pgvector¶
New, Template: search "pgvector" and pick one based on the official
pgvector/pgvector image (for example "Postgres with pgVector Engine").
Name the service Postgres. It gets a volume (Hobby: up to 5 GB; the
database is about 50 MB per 5,000 articles). The app creates the vector
extension itself on first start.
In its Settings, Networking, delete the public TCP proxy: the app reaches
the database over Railway's private network, and nothing else should. The
template's DATABASE_URL is built from that public proxy, so it is empty
afterwards; use DATABASE_URL_PRIVATE (host *.railway.internal), as below.
Do the same for Redis: if its REDIS_URL loses its host after you remove
the proxy, use its private variant instead.
Turn on Backups in the Postgres service (Backups tab): daily. These live with the volume; off-site backups come in step 6.
2.2 Redis¶
New, Database, Redis. Name it Redis.
2.3 Shared variables¶
Project Settings, Shared Variables: add these once, then share them with
the four app services below. The database and Redis addresses are not
here: a shared variable can't reference another service (${{Postgres...}}
resolves to an empty string there), so they go on each service in 2.4.
DEPLOYMENT_MODE=hosted
APP_ENV=production
REGISTRATION_MODE=invite # you sign up first (owner), then invite people
REGISTRATION_INVITE_CODES=pick-a-code,another-code
SECRET_KEY=... # from step 0
REFRESH_SECRET_KEY=...
SECRETS_ENCRYPTION_KEY=...
METRICS_TOKEN=...
FRONTEND_URL=https://weirling.com
CORS_EXTRA_ORIGINS=https://www.weirling.com
PUBLIC_API_URL=https://api.weirling.com
COOKIE_DOMAIN=.weirling.com
LLM_API_KEY=... # paid Groq key
RESEND_API_KEY=... # sending-only key, see 1.5
FROM_EMAIL=hello@mail.weirling.com
REPLY_TO_EMAIL=support@weirling.com # replies to any email land in support@
SUPPORT_EMAIL=support@weirling.com
GRIEVANCE_CONTACT_EMAIL=privacy@weirling.com
NEWSLETTER_INBOX_DOMAIN=inbox.weirling.com
DB_POOL_SIZE=8
DB_MAX_OVERFLOW=4
Billing (BILLING_*) comes later with Lemon Squeezy; leave it out for the
invite-only beta.
2.4 The four app services¶
Each one: New, Docker Image,
ghcr.io/mohitmishra786/weirling/weirling-backend:0.3.0 (or :latest),
attach the shared variables (Variables, Shared Variable, select all), then
in Raw Editor add the four connection variables:
DATABASE_URL="${{Postgres.DATABASE_URL_PRIVATE}}"
REDIS_URL="${{Redis.REDIS_URL}}/0"
CELERY_BROKER_URL="${{Redis.REDIS_URL}}/1"
CELERY_RESULT_BACKEND="${{Redis.REDIS_URL}}/2"
Check them with the eye icon: the hosts must be *.railway.internal. Then
set the start command (Settings, Deploy, Custom Start Command):
| Service | Start command | Notes |
|---|---|---|
| api | sh -c "python -m app.migrate && exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}" |
Settings, Networking: Generate domain first to test, then Custom domain api.weirling.com. Healthcheck path /health. |
| worker-ingest | celery -A app.workers.celery_app worker --loglevel=info --pool=solo -Q scrape,embed -n ingest@%h |
Fetching and embeddings. |
| worker-digest | celery -A app.workers.celery_app worker --loglevel=info --pool=solo -Q digest,celery -n digest@%h |
Kept separate so a slow feed never delays digests. |
| beat | celery -A app.workers.celery_app beat --loglevel=info |
The scheduler. Exactly one replica. |
Then for each of the four: Settings, Source, Auto Updates on (minor and
patch updates if you pinned 0.3.0; new pushes if :latest), with a
maintenance window at night.
To cap cost, set each service's resource limit (Settings, Deploy): api 1 GB, worker-ingest 1.5 GB, worker-digest 1 GB, beat 512 MB.
Check: the api's generated *.up.railway.app URL answers /health with
{"status": "ok", ...}, and the worker logs show ready.
3. Vercel: the web app¶
- Add New, Project: import
mohitmishra786/weirling. - Root Directory:
frontend. Framework: Next.js (detected). Node.js: 24.x (the default). - Environment Variables (Production and Preview):
NEXT_PUBLIC_API_URL=https://api.weirling.com - Deploy. Leave Vercel Analytics and Speed Insights off: the privacy page promises no third-party analytics.
- Settings, Domains: add
weirling.comandwww.weirling.com(redirect www to the apex). Vercel shows the exact DNS records to add.
4. DNS at Spaceship¶
Spaceship, Domains, weirling.com, DNS records. Keep the Spacemail records from step 1; add:
| Type | Host | Value | From |
|---|---|---|---|
| A | @ |
the IP Vercel shows (often 76.76.21.21) |
Vercel, Domains |
| CNAME | www |
the target Vercel shows (e.g. cname.vercel-dns.com) |
Vercel, Domains |
| CNAME | api |
the target Railway shows for the custom domain | Railway, api, Networking |
| TXT | as Railway shows | the verification value | Railway, if it asks |
Changes usually apply within minutes. TLS certificates are issued automatically by Vercel and Railway once the records resolve.
5. First sign-in (do this right away)¶
The first account becomes the instance owner, even with invite-only
sign-up. So, as soon as https://weirling.com loads:
- Register with your own email. You are now the owner.
- Open /admin/setup: use Test for the LLM and for email (it sends you a message through Spacemail).
- Follow a few feeds (or a starter pack), then on Digest press
Generate digest. To see the digest email, open a shell on the api
service (
railway ssh --service apiwith the Railway CLI) and runpython scripts/send_test_digest_email.py you@gmail.com; check it in Gmail and on your phone. - Give testers an invite code from
REGISTRATION_INVITE_CODES.
6. After launch day¶
- Off-site backups (#180): a nightly
pg_dumpto an S3-compatible bucket (Cloudflare R2's free tier is enough) as a Railway cron service. - Status page (#243): a free monitor (Better Stack or UptimeRobot) on
https://api.weirling.com/healthandhttps://weirling.com, published atstatus.weirling.com(one more CNAME). - Newsletter inbox (
you@inbox.weirling.com): an inbound email provider (Resend or Postmark) with MX records on theinboxsubdomain and a webhook tohttps://api.weirling.com/api/v1/newsletter/inbound/resend(or/postmark), signed withRESEND_WEBHOOK_SECRET/POSTMARK_WEBHOOK_SECRET. Spacemail keeps the main domain's mail; the subdomain is separate.
When something is wrong¶
| Symptom | Likely cause |
|---|---|
| Signed in, but every page sends you back to sign-in | COOKIE_DOMAIN isn't .weirling.com, or FRONTEND_URL doesn't match the site's address |
| The browser console shows CORS errors | FRONTEND_URL / CORS_EXTRA_ORIGINS don't list the exact origin (with https://) |
DATABASE_URL is empty or Could not parse SQLAlchemy URL |
the reference resolved to nothing: set DATABASE_URL=${{Postgres.DATABASE_URL_PRIVATE}} on the service itself (not as a shared variable) and check the value with the eye icon |
Healthcheck fails with "service unavailable" although the log says Uvicorn running |
uvicorn listens on IPv6 only (--host ::); use --host 0.0.0.0 |
| api restarts during deploy | the migration failed: check the api logs right after python -m app.migrate |
| Sign-up or "Resend the link" hangs about 30 s and no email arrives | SMTP is blocked by the host (Railway Hobby): use Resend (1.5) |
| No digests arrive | worker-digest or beat isn't running, or SMTP is wrong: test it in /admin/setup |
| "too many clients" from Postgres | lower DB_POOL_SIZE / DB_MAX_OVERFLOW (workers don't pool) |