Skip to content

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:

  1. Wildcard subdomain support — a single Vercel project can serve *.restaurant-os.com (every tenant subdomain) through one deployment, with src/proxy.ts doing the per-request tenant resolution described in 02.
  2. 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

EnvironmentBranchPurposeDomain
ProductionmainLive trafficrestaurant-os.com, *.restaurant-os.com + verified custom domains
Previewany PR branchPer-PR review deploys (Vercel auto-generates)<project>-<hash>.vercel.app
Developmentlocalnpm 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 ci and 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.ts normally builds as part of its webServer command, which would have made a second production build inside the same job. CI sets PLAYWRIGHT_SKIP_BUILD=1 so 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 running test:e2e after 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 dev locally during active schema development, generating timestamped migration SQL files committed to the repo (prisma/migrations/).
  • prisma migrate deploy in CI/CD for staging/production — never migrate dev outside 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.featureFlagIds nullable-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

VariableScopeNotes
DATABASE_URLServer-onlySupabase Postgres connection string (pooled, PgBouncer transaction mode)
UPSTASH_REDIS_REST_URL / _TOKENServer-onlyCaching + rate limiting
VERCEL_API_TOKEN / VERCEL_PROJECT_IDServer-onlyCustom domain automation
BKASH_APP_KEY / BKASH_APP_SECRETServer-onlyPayment gateway credentials
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRETServer-onlyPayment gateway credentials
NEXT_PUBLIC_APP_URLPublicBase 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) জন্য আলাদাভাবে — কখনোই কোডে কমিট করা হবে না।