10 — DevOps & CI/CD Pipeline
Status: 🟡 Partly built — the CI pipeline in §3 exists and runs (.github/workflows/ci.yml). Everything else here is target design: no hosted deployment, no Vercel project, no database to migrate.
🟢 বাংলা ভূমিকা: এই ডকুমেন্টে Vercel ডিপ্লয়মেন্ট, Vercel Domains API দিয়ে কাস্টম ডোমেইন অটোমেশন, GitHub Actions CI/CD পাইপলাইন, এবং Prisma মাইগ্রেশন কৌশল বর্ণনা করা হয়েছে।
1. Hosting: Vercel
Next.js 16 + Vercel is the natural pairing for this architecture specifically because of two platform features this project depends on:
- Wildcard subdomain support — a single Vercel project can serve
*.restaurant-os.com(every tenant subdomain) through one deployment, withsrc/proxy.tsdoing the per-request tenant resolution described in 02. - Vercel Domains API — allows programmatically attaching a tenant's custom domain (e.g.
menu.kacchibhai.com) to the project the moment an owner enters it in Settings (04 §13), without a manual dashboard step per tenant.
// lib/domains/attach-custom-domain.ts
export async function attachCustomDomain(tenantId: string, domain: string) {
const res = await fetch(`https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ name: domain }),
});
const data = await res.json();
// data.verification contains the CNAME/TXT records the owner must add at their DNS provider
await prisma.tenant.update({ where: { id: tenantId }, data: { customDomain: domain, domainStatus: "pending_verification" } });
return data.verification;
}
// Polled (or webhook-driven, via Vercel's domain-events webhook) until Vercel reports the domain verified:
export async function checkDomainVerification(domain: string) {
const res = await fetch(`https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain}`, {
headers: { Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}` },
});
const data = await res.json();
return data.verified === true;
}
Of the four steps — (1) enter domain → (2) show the CNAME record to add at their registrar → (3) confirm verification → (4) flip Tenant.customDomain live — the first three are built on the settings card, and step 3 is a real DNS-over-HTTPS lookup against siteConfig.customDomainTarget rather than a button that pretends. Step 4 is not: it needs attachCustomDomain above and a persisted tenant record, because proxy.ts resolves hosts on the server and cannot see anything the owner saved in their browser. (This paragraph previously claimed the whole flow was already built when none of it was.)
🟢 বাংলা সারাংশ: Vercel-এ এই প্রজেক্ট হোস্ট করার প্রধান কারণ দুটি — (১) ওয়াইল্ডকার্ড সাবডোমেইন সাপোর্ট (একটাই ডিপ্লয়মেন্টে সব *.restaurant-os.com সাবডোমেইন সার্ভ করা যায়), এবং (২) Vercel Domains API যা কাস্টম ডোমেইন যোগ করাকে সম্পূর্ণ স্বয়ংক্রিয় করে — মালিক ড্যাশবোর্ড থেকে ডোমেইন দিলে সিস্টেম নিজে থেকেই CNAME রেকর্ড দেখায় ও ভেরিফিকেশন চেক করে।
2. Environments
| Environment | Branch | Purpose | Domain |
|---|---|---|---|
| Production | main | Live traffic | restaurant-os.com, *.restaurant-os.com + verified custom domains |
| Preview | any PR branch | Per-PR review deploys (Vercel auto-generates) | <project>-<hash>.vercel.app |
| Development | local | npm run dev against local .env.local (Supabase local stack or a shared dev project) | localhost:3000 (subdomains simulated via /etc/hosts or a query param override) |
Local multi-tenant testing without real DNS: proxy.ts should accept an ?tenant= override query param (or read x-forwarded-host in dev) so contributors can test tenant routing on localhost:3000 without configuring wildcard local DNS — this convention should be preserved/extended, not replaced, once real domains are live.
🟢 বাংলা সারাংশ: তিনটি এনভায়রনমেন্ট — Production (main ব্রাঞ্চ), Preview (প্রতিটি PR-এর জন্য Vercel অটো-জেনারেটেড), এবং Development (লোকাল)। লোকালে মাল্টি-টেন্যান্ট টেস্ট করার জন্য রিয়েল DNS ছাড়াই একটা query param বা override মেকানিজম ব্যবহার করা হবে।
3. GitHub Actions CI Pipeline
✅ Built — .github/workflows/ci.yml runs on every push to main and
every pull request. It exists because Rule 8's gates ran nowhere but the
machine of whoever wrote the change: husky's pre-push covers tsc and the
unit suite, and nothing ran the build or the fifty end-to-end tests unless
someone remembered to.
What actually shipped differs from the sketch below in three ways, each for a reason worth keeping:
- One job, not two. Every gate needs the same
npm ciand four of them need the same production build. Splitting them pays for both several times over to save nothing on a suite this size. The Prisma job is absent because there is no database. - E2E is not gated behind a label. The sketch made it opt-in "to keep default CI fast"; the whole suite is ten seconds against a warm build, so the only thing a label would buy is a merged PR nobody ran it on.
- The build is made once.
playwright.config.tsnormally builds as part of itswebServercommand, which would have made a second production build inside the same job. CI setsPLAYWRIGHT_SKIP_BUILD=1so the suite starts the build the previous step already made — and so the artefact under test is the one the build gate passed. Locally the flag is unset and the build stays part of the command, because runningtest:e2eafter an edit should test the edit rather than whatever was left in.next.
npm run format:check is in there too: husky formats staged files, so a
commit made with --no-verify can still land unformatted.
The sketch this replaced:
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
lint-type-test-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npm run test -- --run # Vitest unit/component tests, see 11-testing-and-qa-strategy.md
- run: npm run build
- run: npx playwright install --with-deps && npm run test:e2e # gated behind a `e2e` label on PRs to keep default CI fast
migrate-preview-db:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx prisma migrate deploy # against an ephemeral/branch Supabase preview database (Supabase branching)
Vercel's own GitHub integration handles the actual deploy (preview on every PR, production on merge to main) independently of this workflow — this CI pipeline's job is purely to gate merges on lint/type/test/build passing, and (optionally) apply Prisma migrations to a matching preview database branch so PR previews reflect schema changes too.
🟢 বাংলা সারাংশ: GitHub Actions পাইপলাইন এখন সত্যিই আছে এবং চলে — প্রতিটি push ও PR-তে টাইপ চেক, lint, ফরম্যাট চেক, ১৯১টি ইউনিট টেস্ট, বিল্ড আর ৫০টি E2E টেস্ট। আগে এগুলোর মধ্যে শুধু tsc আর ইউনিট টেস্ট চলত, তাও যিনি কোড লিখছেন তাঁর নিজের মেশিনে (husky pre-push); বিল্ড আর E2E কোথাও চলত না। উপরের স্কেচ থেকে তিনটি পার্থক্য: এক জব (দুটি নয়), E2E লেবেলের পেছনে লুকানো নয় (পুরো সুইট দশ সেকেন্ড), আর বিল্ড একবারই হয় — PLAYWRIGHT_SKIP_BUILD=1 দিয়ে E2E সেই বিল্ডটাই চালায় যেটা বিল্ড গেট পাস করেছে। প্রকৃত ডিপ্লয়মেন্ট এখনো নেই।
4. Prisma Migration Strategy
prisma migrate devlocally during active schema development, generating timestamped migration SQL files committed to the repo (prisma/migrations/).prisma migrate deployin CI/CD for staging/production — nevermigrate devoutside a developer's machine, since it can prompt for destructive resets.- Supabase branching (or a scripted ephemeral database per PR) so preview deployments can run against a schema-matched database rather than production, catching migration issues before merge.
- Backward-compatible migration discipline: additive changes (new nullable column, new table) ship independently of the application code change that starts using them; destructive changes (drop column, tighten a nullable→required constraint) require a two-step migration — first stop writing to the old shape, deploy, confirm no reads depend on it, then drop it in a follow-up migration. This matters especially for the
SubscriptionPlan.featureFlagIdsnullable-whitelist semantic documented in 07 §3 — tightening that column incorrectly would silently break every tenant on a legacy seed plan.
🟢 বাংলা সারাংশ: স্কিমা পরিবর্তনের জন্য prisma migrate dev লোকালে এবং prisma migrate deploy CI/CD-তে ব্যবহার হবে। Preview ডিপ্লয়মেন্টের জন্য Supabase branching বা আলাদা ডাটাবেজ ব্যবহার করা হবে। বিশেষভাবে সতর্ক থাকতে হবে ধ্বংসাত্মক পরিবর্তনের ক্ষেত্রে (যেমন কলাম বাদ দেওয়া) — দুই ধাপে করতে হবে, বিশেষ করে featureFlagIds কলামের nullable সেমান্টিক্স ভুলভাবে পরিবর্তন করলে পুরনো টেন্যান্টদের ফিচার হারিয়ে যেতে পারে।
5. Secrets & Environment Variable Management
| Variable | Scope | Notes |
|---|---|---|
DATABASE_URL | Server-only | Supabase Postgres connection string (pooled, PgBouncer transaction mode) |
UPSTASH_REDIS_REST_URL / _TOKEN | Server-only | Caching + rate limiting |
VERCEL_API_TOKEN / VERCEL_PROJECT_ID | Server-only | Custom domain automation |
BKASH_APP_KEY / BKASH_APP_SECRET | Server-only | Payment gateway credentials |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | Server-only | Payment gateway credentials |
NEXT_PUBLIC_APP_URL | Public | Base URL for constructing absolute links (QR codes, promo shares, callback URLs) |
All server-only secrets are managed via Vercel's encrypted environment variable store (scoped per environment: Production/Preview/Development), never committed to the repo — .env.local (gitignored) is used for local development only.
🟢 বাংলা সারাংশ: সব সার্ভার-সাইড সিক্রেট (ডাটাবেজ URL, Redis টোকেন, পেমেন্ট গেটওয়ে কী) Vercel-এর এনক্রিপ্টেড এনভায়রনমেন্ট ভ্যারিয়েবল স্টোরে রাখা হবে, প্রতিটি এনভায়রনমেন্টের (Production/Preview/Development) জন্য আলাদাভাবে — কখনোই কোডে কমিট করা হবে না।