Deployment
This guide covers every external service you sign up for to take the project from “running locally on my laptop” to “publicly hosted with real users”. Everything is free-tier where possible.
Order matters — later steps need values from earlier ones. Do them top-to-bottom.
What you’ll provision
| Service | Why | Free? |
|---|---|---|
| GitHub | Source repo, CI, Releases | yes |
| Neon | Serverless Postgres for prod registry + telemetry | yes (0.5 GB) |
| Cloudflare R2 | Object storage for uploaded mod .zip files | yes (10 GB/mo egress free) |
| Vercel | Hosting for apps/www (Next.js), apps/api (Hono/Node), and apps/docs (Astro/Starlight) — three projects, same repo | yes |
| Domain (rsmm.me or similar) | Friendly URL + email | ~$10/yr |
If money is the issue, everything runs on Vercel’s Hobby tier. Skip the
domain for v1 and use the *.vercel.app URLs.
Step 1: GitHub repo + Actions secrets
- Push the monorepo to GitHub if you haven’t already:
Terminal window git remote -v# if no origin: gh repo create Ovilli/RavenswatchModManager --source=. --public --pushgit push -u origin main - Open github.com/Ovilli/RavenswatchModManager → Settings → Secrets and variables → Actions.
- You’ll come back to add secrets here in later steps. Leave this tab open.
The release workflow (.github/workflows/release.yml) already runs on
git push --tags v* — no secrets needed for basic Tauri builds; the
default GITHUB_TOKEN is enough for creating the draft release.
- (Optional) Discord release notifications:
- Create a webhook in your Discord server (Server Settings → Integrations → Webhooks).
- Copy the webhook URL and add it as a repository secret:
Settings → Secrets and variables → Actions → New repository secret
- Name:
DISCORD_WEBHOOK_URL - Value:
https://discord.com/api/webhooks/...
- Name:
- Tag-driven releases notify Discord from the
finalize-releasejob inrelease.yml(GitHub does not firerelease: publishedfor publishes done viaGITHUB_TOKEN). Manual/UI publishes still triggerdiscord-notify.yml.
Step 2: Neon (serverless Postgres)
- Sign up at https://neon.tech (GitHub login is fastest).
- Click “New Project”:
- Name:
rsmm-prod - Postgres version: 16
- Region: pick whatever’s closest to where your API will run (see Step 5)
- Name:
- After creation, Neon shows a Connection string. It looks like:
Copy it. This ispostgresql://rsmm_owner:abc123@ep-cool-snow-12345.us-east-2.aws.neon.tech/rsmm?sslmode=require
DATABASE_URL. - In your Neon project, go to Branches → main → Database. The default
neondbmay be the DB name; create one calledrsmmvia Tables → New database if you prefer a cleaner name. Update the connection string accordingly. - Apply the schema once from your laptop, against the Neon URL:
Terminal window DATABASE_URL='paste-neon-url' DB_DRIVER=neon pnpm db:push
You now have a production DB. Save the connection string somewhere secure (1Password, Bitwarden) — you’ll paste it into the Vercel env vars in later steps.
Step 3: Cloudflare R2 (object storage for uploads)
R2 is S3-compatible. The API code already supports it via the same
S3_* env vars.
- Sign up at https://dash.cloudflare.com.
- Left sidebar → R2 Object Storage. Click Purchase R2 — the free tier costs nothing but Cloudflare wants a card on file. Free tier: 10 GB storage + 10M Class A ops + unlimited egress.
- Click Create bucket:
- Name:
rsmm-mods - Location: Automatic
- Name:
- After creation, open the bucket → Settings. Note the bucket name + the Account ID (top of the page or right sidebar).
- Left sidebar → R2 → Manage R2 API Tokens → Create API token:
- Token name:
rsmm-api-write - Permissions: Object Read & Write
- Specify bucket:
rsmm-mods - TTL: leave blank (or set to 1 year)
- Click Create API Token
- Token name:
- R2 shows the credentials once — copy these now:
- Access Key ID
- Secret Access Key
- Endpoint for S3-compatible clients:
https://<account-id>.r2.cloudflarestorage.com
- (Optional) Enable a public r2.dev URL for the bucket so clients can download mods without signed URLs:
- Bucket → Settings → Public access → Allow Access → opt into
r2.dev - Note the public URL:
https://pub-<hash>.r2.dev - This is
S3_PUBLIC_BASE_URL. Skip if you want all downloads gated by signed URLs.
- Bucket → Settings → Public access → Allow Access → opt into
You’ll paste these into the API env in Step 5:
S3_BUCKET=rsmm-modsS3_REGION=autoS3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.comS3_ACCESS_KEY_ID=<from step 6>S3_SECRET_ACCESS_KEY=<from step 6>S3_PUBLIC_BASE_URL=https://pub-<hash>.r2.dev # only if Step 7 doneStep 4: Generate the Better Auth secret
You already have one in your local .env. For prod, generate a fresh one:
openssl rand -hex 32Copy the 64-char hex string. This is BETTER_AUTH_SECRET for the API
host (Step 5). Never commit it.
Step 5: Vercel (host for apps/api)
The API is a Node Hono server, deployed as its own Vercel project.
apps/api/vercel.json already pins the install and build commands
(pnpm --filter api vercel-build), so the dashboard setup is minimal.
- Sign up at https://vercel.com, GitHub login.
- Go to https://vercel.com/new and Import the
RavenswatchModManagerrepo as a new project (separate from theapps/wwwandapps/docsprojects). - In Configure Project:
- Root Directory:
apps/api(click Edit → select the folder). Vercel still installs the whole pnpm workspace from the repo root. - Build Command / Install Command: leave as-is —
vercel.jsonprovides them.
- Root Directory:
- Environment Variables (Production + Preview):
DATABASE_URL=postgresql://...neon.tech/rsmm?sslmode=requireDB_DRIVER=neonBETTER_AUTH_SECRET=<from step 4>BETTER_AUTH_URL=https://<your-api-project>.vercel.appTRUSTED_ORIGINS=https://www.rsmm.me,https://rsmm.vercel.app,tauri://localhostS3_BUCKET=rsmm-modsS3_REGION=autoS3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.comS3_ACCESS_KEY_ID=<from step 3>S3_SECRET_ACCESS_KEY=<from step 3>S3_PUBLIC_BASE_URL=https://pub-<hash>.r2.dev
- Click Deploy.
- Verify:
Terminal window curl https://<your-api-project>.vercel.app/health# → {"ok":true,"ts":...}
The API is now live. Save that URL — the www project and the desktop app need it. Remember: Vercel does not apply env-var changes to a running deployment — redeploy after every env edit.
Step 5b: Social sign-in (optional — Google / GitHub)
The “Continue with Google” / “Continue with GitHub” buttons are already
built into the sign-in and sign-up pages. They stay hidden until the
matching OAuth credentials exist in the API env — /api/auth-config
reports which providers are configured and the UI renders only those. No
code change is needed to enable them.
The callback URL must be
<BETTER_AUTH_URL>/api/auth/callback/<provider>whereBETTER_AUTH_URLis whatever the deployed API actually serves under. If the API runs on Vercel (the current setup) athttps://api.rsmm.me, the callback ishttps://api.rsmm.me/api/auth/callback/google. A mismatch here is the #1 cause ofError 400: redirect_uri_mismatch. ChangingBETTER_AUTH_URLlater means updating BOTH the env var and the console.
To turn on Google:
- Google Cloud Console → APIs & Services → Credentials → Create Credentials → OAuth client ID. Application type: Web application.
- Under Authorized redirect URIs, add (must match
BETTER_AUTH_URLexactly — no trailing slash):Add a second entryhttps://api.rsmm.me/api/auth/callback/googlehttp://localhost:3001/api/auth/callback/googleif you want Google sign-in to work in local dev too — and run the API locally withBETTER_AUTH_URL=http://localhost:3001so the generated callback matches (you can’t test locally whileBETTER_AUTH_URLpoints at prod). - Set the generated client ID + secret on the API host. On Vercel:
Project → Settings → Environment Variables → add
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET(Production + Preview), then redeploy — Vercel does not apply env changes to the running deployment until you redeploy. - After the redeploy, reload
/auth/signin— the Google button appears once/api/auth-configreportsgoogle: true.
GitHub is identical: register an OAuth App at
GitHub → Settings → Developer settings → OAuth Apps, set the callback to
https://api.rsmm.me/api/auth/callback/github, then set
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET the same way and redeploy.
Both providers are off by default and safe to skip — email/password sign-in works without them.
Step 6: Vercel (host for apps/www)
- Sign up at https://vercel.com, GitHub login.
- Click Add New → Project. Pick the
RavenswatchModManagerrepo. - Vercel detects Next.js. Override these settings:
- Root Directory:
apps/www - Build Command:
cd ../.. && pnpm install --frozen-lockfile && pnpm --filter www build - Install Command:
pnpm install --filter www... - Framework Preset: Next.js
- Root Directory:
- Environment Variables:
NEXT_PUBLIC_API_URL=https://<your-api-project>.vercel.app(from Step 5)
- Click Deploy. First build takes ~2 min.
- Vercel gives you a URL like
rsmm.vercel.app. Open it. The landing page should load;/auth/signinshould work because it talks to your API project.
If sign-in fails with CORS errors: go back to Step 5 and make sure your
Vercel www URL is in the API project’s TRUSTED_ORIGINS, then redeploy
the API to pick up the env change.
Step 7: Vercel (host for apps/docs)
The docs are a static Astro/Starlight site. apps/docs/vercel.json already
pins the framework, install, build, and output — so the only thing you set in
the dashboard is the Root Directory.
- Go to https://vercel.com/new and Import the
RavenswatchModManagerrepo as a new project (separate from theapps/wwwproject). - In Configure Project:
- Root Directory:
apps/docs(click Edit → select the folder). Vercel still installs the whole pnpm workspace from the repo root. - Framework Preset: Astro (auto-detected;
vercel.jsonalso sets it). - Build Command / Output Directory / Install Command: leave as-is —
vercel.jsonprovides them (pnpm --filter docs build→dist).
- Root Directory:
- Click Deploy.
You’ll get a URL like rsmm-docs.vercel.app.
The build runs
starlight-links-validator; a broken internal link fails the deploy, same as CI. Fix links locally withpnpm --filter docs buildbefore pushing.
Step 8: Custom domain (optional but recommended)
Buy rsmm.me (or whatever) from Cloudflare Registrar (cheapest,
no upsells) or Namecheap.
After registering at Cloudflare:
| Subdomain | Points to | How |
|---|---|---|
rsmm.me (apex) | Vercel (www) | Vercel project → Settings → Domains → add rsmm.me → Cloudflare DNS: A @ 76.76.21.21 |
www.rsmm.me | Vercel (www) | Vercel adds this automatically with the apex |
api.rsmm.me | Vercel (api) | Vercel api project → Settings → Domains → add api.rsmm.me → add the CNAME it shows in your DNS |
docs.rsmm.me | Vercel (docs) | Vercel docs project → Settings → Domains → add docs.rsmm.me → add the CNAME it shows in your DNS |
cdn.rsmm.me | R2 public bucket | R2 bucket → Settings → Custom Domains → cdn.rsmm.me |
After domain is live, update (api project env, then redeploy the api):
BETTER_AUTH_URL→https://api.rsmm.meTRUSTED_ORIGINS→https://rsmm.me,https://www.rsmm.me,tauri://localhostS3_PUBLIC_BASE_URL→https://cdn.rsmm.me- www project env
NEXT_PUBLIC_API_URL→https://api.rsmm.me+ redeploy www
Step 9: Desktop app distribution
Tag a release to trigger the tauri-action workflow:
git tag v0.1.0git push origin v0.1.0GitHub Actions builds installers for Windows / Linux and posts them as a draft GitHub Release. Visit the release page, edit the notes, click Publish.
Users download from github.com/Ovilli/RavenswatchModManager/releases.
For code signing (Windows SmartScreen), you’ll need a paid cert (~$200/yr). Skip it until the project has users.
Step 10: Production env summary
When everything is live, this is what each host runs:
Vercel (api — api.rsmm.me)
DATABASE_URL=postgresql://...neon.tech/rsmm?sslmode=requireDB_DRIVER=neonBETTER_AUTH_SECRET=<random 64 hex>BETTER_AUTH_URL=https://api.rsmm.meTRUSTED_ORIGINS=https://rsmm.me,https://www.rsmm.me,tauri://localhostS3_BUCKET=rsmm-modsS3_REGION=autoS3_ENDPOINT=https://<account>.r2.cloudflarestorage.comS3_ACCESS_KEY_ID=...S3_SECRET_ACCESS_KEY=...S3_PUBLIC_BASE_URL=https://cdn.rsmm.meVercel (rsmm.me)
NEXT_PUBLIC_API_URL=https://api.rsmm.meVercel (docs — docs.rsmm.me)
No environment variables required — the docs site is fully static. Vercel auto-selects a recent Node version; pin it under Settings → General → Node.js Version if you want to match CI (22).
Desktop installer (built by tauri-action) — talks to whatever
VITE_API_URL is baked in at build time. For a release pointing at
prod, add to .github/workflows/release.yml:
- name: Build desktop uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_API_URL: https://api.rsmm.meSmoke test checklist
After every step:
- Step 2:
psql 'postgresql://...neon.tech/...?sslmode=require' -c '\dt'lists tables - Step 3:
curl -X PUT 'https://<endpoint>/rsmm-mods/test.txt' -H 'x-amz-content-sha256: UNSIGNED-PAYLOAD' -H 'authorization: ...'returns 200 (or use a tool like rclone) - Step 5:
curl https://<your-api-project>.vercel.app/health→{"ok":true} - Step 5:
curl https://<your-api-project>.vercel.app/mods→{"items":[],"total":0} - Step 6:
curl https://rsmm.vercel.app/returns HTML - Step 6: open
https://rsmm.vercel.app/auth/signup, create account, check Neonusertable has row - Step 7: open
docs.rsmm.me→ Starlight site loads
If any step fails, the rest will fail too. Don’t skip ahead.
Cost reality check
| Item | Free tier holds up to | Paid trigger |
|---|---|---|
| Neon | ~100 active users, 0.5 GB | $19/mo Pro |
| R2 | 10 GB, 10M Class A ops/mo | $0.015/GB after |
| Vercel (www + api) | 100 GB egress, no commercial use on free | $20/mo Pro |
| Vercel (docs) | static, shares the Vercel Hobby free tier | $20/mo Pro |
| GitHub Actions | 2000 min/mo private (unlimited public) | $0 for public repos |
| Domain | n/a | $10/yr |
Total for a small launch: ~$0–15/mo + $10/yr domain.