CI/CD

The CI/CD pipeline is built on GitHub Actions for automation (lint, type-check, test, deploy) and Vercel for hosting and deployment. The main branch maps to the production environment.


1. Repository

Attribute Value
Platform GitHub
Default branch main (production)
Development branch dev (preview)
PR target dev โ†’ PR review โ†’ merge to main
Access Private repository

Branch protection rules

  • main: Require PR review, require status checks to pass (lint, type-check, build), no direct push from sandbox
  • dev: Require status checks to pass, direct push allowed from local dev

2. GitHub Actions Workflows

ci.yml โ€” Pull Request checks

Runs on every PR targeting dev or main. Must pass before merge is allowed.

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main, dev]

jobs:
  lint-and-typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Lint
        run: npm run lint
      - name: Type check
        run: npm run type-check  # runs tsc --noEmit

  build:
    runs-on: ubuntu-latest
    needs: lint-and-typecheck
    env:
      NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
      NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Build
        run: npm run build

deploy-preview.yml โ€” Preview deployment

Runs on push to dev. Deploys to a Vercel preview environment.

# .github/workflows/deploy-preview.yml
name: Deploy Preview

on:
  push:
    branches: [dev]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Deploy to Vercel (preview)
        run: npx vercel deploy --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

deploy-production.yml โ€” Production deployment

Runs on push to main. Deploys to Vercel production.

# .github/workflows/deploy-production.yml
name: Deploy Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Deploy to Vercel (production)
        run: npx vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

e2e.yml โ€” End-to-End tests

Runs on PRs to main. Uses Playwright against the preview deployment URL.

# .github/workflows/e2e.yml
name: E2E Tests

on:
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium
      - name: Run E2E tests
        run: npx playwright test
        env:
          BASE_URL: ${{ steps.deploy.outputs.preview_url }}
          E2E_ADMIN_EMAIL: ${{ secrets.E2E_ADMIN_EMAIL }}
          E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}

3. Vercel Integration

Vercel is connected to the GitHub repository via the Vercel GitHub App. This enables:

  • Automatic preview deployments for every PR (unique URL per PR)
  • Production deployment on merge to main (via GitHub Actions --prod flag)
  • Build log streaming in the Vercel dashboard and in PR checks

Vercel project settings

Setting Value
Framework preset Next.js
Build command next build
Output directory .next
Install command npm ci
Node.js version 20.x
Root directory / (monorepo root)

4. Environment Variables

Environment variables are set in the Vercel dashboard under Project โ†’ Settings โ†’ Environment Variables. They are never committed to the repository.

๐Ÿšจ
Do not commit secrets

.env.local is in .gitignore. Never add SUPABASE_SERVICE_ROLE_KEY, STRIPE_SECRET_KEY, or RESEND_API_KEY to any file tracked by git.

For CI builds that need public variables (build step), secrets are duplicated as GitHub Actions secrets with the NEXT_PUBLIC_ prefix where required.


5. Build Command

next build

This compiles the Next.js application, runs static analysis for page/route errors, and produces the .next output directory. The build will fail if:

  • TypeScript compilation errors exist (Next.js runs tsc during build)
  • An API route or page is missing export const runtime = 'edge' AND uses a Node.js-only API (caught at build time)
  • A dynamic import fails to resolve

6. Edge Runtime Enforcement

All route files and pages must export:

export const runtime = 'edge'

This is enforced at build time by Next.js (Node.js-incompatible APIs cause a build error) and by a custom ESLint rule:

// .eslintrc.js (custom rule excerpt)
// Warn if any file in app/api/** or app/**/page.tsx is missing the runtime export

Common edge-runtime incompatible APIs that cause build failures:

  • fs module (no filesystem in edge runtime)
  • Buffer (use Uint8Array instead)
  • process.cwd() (not available in edge)
  • Node.js crypto (use Web Crypto API: crypto.subtle)

7. Type Checking

Type checking runs separately from the build using:

npm run type-check
# Equivalent to: tsc --noEmit

tsconfig.json is set to "strict": true. The most common type errors in this codebase relate to Supabase nested FK joins returning arrays (see Supabase Gotchas).


8. Linting

npm run lint
# Equivalent to: next lint

ESLint is configured via .eslintrc.js with: - eslint-config-next (Next.js defaults) - @typescript-eslint (TypeScript-specific rules) - Custom rule for runtime = 'edge' enforcement

Linting failures block merge. Fix all errors before opening a PR.


9. FUSE-Safe Commit Pattern

โš ๏ธ
Required when committing from a sandbox or FUSE-mounted environment

When working in a FUSE-mounted workspace (e.g., Claude Code sandbox, certain CI environments), standard git add and git checkout commands cause file corruption. Use the git plumbing pattern described below. See Deployment for the full step-by-step.

Why FUSE causes problems:

  • FUSE silently truncates large files on write operations
  • git add from a FUSE path can produce a truncated blob
  • The truncation affects both the on-disk file and the git object โ€” standard verification (wc -l vs git cat-file) will not catch it because both are truncated identically
  • The corrupted commit passes local checks but GitHub rejects the push with an object pack error

The safe pattern in brief:

  1. Write file content to /tmp (not the FUSE mount) using Python
  2. Hash the file from /tmp using git hash-object -w --stdin with Python subprocess
  3. Verify blob line count: git cat-file -p <sha> | wc -l must match wc -l /tmp/filename
  4. Build trees using Python (never printf | git mktree โ€” causes duplicate entries)
  5. Create commit using git commit-tree with the last pushed SHA as parent
  6. Update refs/heads/main via Python file write
  7. Push using explicit SHA: git push origin <sha>:refs/heads/main

Full detail: Deployment โ†’ FUSE-Safe Commit Procedure


10. Deployment Verification

After every production deployment, verify the following:

  • [ ] Vercel deployment status shows "Ready" (not "Error" or "Building")
  • [ ] https://app.sonandigital.com loads with HTTP 200
  • [ ] Admin login flow completes successfully
  • [ ] One API route responds correctly (e.g., GET /api/admin/notifications)
  • [ ] Sentry receives a test event (check Sentry dashboard for recent activity)
  • [ ] Cron jobs are listed in Vercel Dashboard โ†’ Cron Jobs with correct schedules
  • [ ] No new errors in Vercel function logs in the first 10 minutes post-deploy

See Deployment for the full checklist.


Deployment

This page covers everything needed to deploy the SONAN DIGITAL CRM to production: Vercel configuration, environment variables, the FUSE-safe commit procedure, rollback, and the post-deployment checklist.


1. Vercel Configuration

The project is deployed on Vercel. The GitHub integration triggers builds automatically on push to the configured branches.

Setting Value
Team SONAN DIGITAL
Project name sonan-digital-crm
Framework Next.js
Region Washington D.C. (iad1) โ€” closest to Supabase US East
Build command next build
Output directory .next (auto-detected)
Install command npm ci
Node.js version 20.x
Root directory /

Vercel project dashboard

Access via: vercel.com/sonan-digital/sonan-digital-crm


2. Edge Runtime Requirement

๐Ÿšจ
Every route and page must export runtime = 'edge'
```typescript
export const runtime = 'edge'
```

Place this at the top of every `route.ts`, `page.tsx`, and `layout.tsx` file. Missing this on a route that uses Web-only APIs is caught at build time. Missing it on a route that accidentally uses a Node.js API may only fail at runtime in production.

**Vercel will not warn you during deployment if a route silently falls back to Node.js runtime.** Check the Functions tab in the Vercel dashboard after deployment to verify all functions are listed as Edge Functions.

3. Environment Variables

Set these in the Vercel dashboard under Project โ†’ Settings โ†’ Environment Variables. Apply to Production, Preview, and Development as appropriate.

Variable Environment Description
NEXT_PUBLIC_SUPABASE_URL All Supabase project URL. Value: {{ NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY All Supabase anon/public key. Value: {{ NEXT_PUBLIC_SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY All Supabase service role key (server-only). Value: {{ SUPABASE_SERVICE_ROLE_KEY }}
RESEND_API_KEY All Resend email API key. Value: {{ RESEND_API_KEY }}
STRIPE_SECRET_KEY All Stripe secret key. Value: {{ STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET All Stripe webhook signing secret. Value: {{ STRIPE_WEBHOOK_SECRET }}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY All Stripe publishable key (browser-safe). Value: {{ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
CRON_SECRET All Shared secret for cron endpoint authentication. Value: {{ CRON_SECRET }}
NEXT_PUBLIC_SENTRY_DSN All Sentry DSN for client-side error reporting. Value: {{ NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN Production, Preview Sentry auth token for source map uploads. Value: {{ SENTRY_AUTH_TOKEN }}
NEXT_PUBLIC_APP_URL All Public base URL. Production: https://app.sonandigital.com
โ„น๏ธ
These are already configured

All variables listed above are already set in the Vercel dashboard. Do not add them again or ask team members to re-enter them unless a key has been rotated.


4. Domain Configuration

Domain Target SSL
app.sonandigital.com Vercel production deployment Auto-managed by Vercel (Let's Encrypt)
sonandigital.com Marketing site (separate project) โ€”

DNS is configured to point app.sonandigital.com to Vercel's nameservers. SSL certificates are auto-renewed by Vercel. No manual certificate management is required.


5. FUSE-Safe Commit Procedure

โš ๏ธ
This procedure is required when committing from a FUSE-mounted workspace

Standard git add, git checkout, and direct file writes to the FUSE mount path silently truncate large files. The truncation affects both the disk copy and the git blob simultaneously, so wc -l verification does not catch it. Use the procedure below without shortcuts.

Why this happens

FUSE (Filesystem in Userspace) overlays the real filesystem with a virtual layer. Large sequential writes to a FUSE-mounted path may be silently truncated at a buffer boundary. The truncation is not reported as an error by the OS, so write() returns success even though data was lost.

Step-by-step procedure

A. Write file content to /tmp (non-FUSE path) using Python

import subprocess

content = r"""
...full file content here...
"""

with open('/tmp/target-file.tsx', 'w') as f:
    f.write(content)

print(f"Wrote {len(content)} characters to /tmp/target-file.tsx")

Never write directly to the FUSE-mounted workspace path. Always use /tmp.

B. Hash the file from /tmp using Python subprocess

with open('/tmp/target-file.tsx', 'rb') as f:
    data = f.read()

result = subprocess.run(
    ['git', 'hash-object', '-w', '--stdin'],
    input=data,
    capture_output=True,
    cwd='/path/to/fuse/repo'
)

blob_sha = result.stdout.decode().strip()
print(f"Blob SHA: {blob_sha}")
โ„น๏ธ
Harmless FUSE warning

You may see: unable to unlink tmp_obj_*. This is a FUSE permission warning during hash-object. The blob IS written successfully. Verify with git cat-file -t <sha>.

C. Immediately verify blob line count (mandatory)

# Both numbers must match exactly
git cat-file -p <blob_sha> | wc -l
wc -l /tmp/target-file.tsx

If the numbers differ, the blob is truncated. Do not proceed. Re-hash before continuing.

D. Build trees using Python helpers

Never use printf | git mktree from shell โ€” it causes duplicate tree entries that GitHub rejects.

def ls_tree(treeish, repo_path):
    result = subprocess.run(
        ['git', 'ls-tree', treeish],
        capture_output=True, text=True, cwd=repo_path
    )
    entries = []
    for line in result.stdout.strip().split('\n'):
        if not line.strip():
            continue
        meta, name = line.split('\t', 1)
        mode, typ, sha = meta.split()
        entries.append((mode, typ, sha, name))
    return entries

def mktree(entries, repo_path):
    # Always sort before mktree
    sorted_entries = sorted(entries, key=lambda x: x[3])
    lines = '\n'.join(
        f"{mode} {typ} {sha}\t{name}"
        for mode, typ, sha, name in sorted_entries
    )
    result = subprocess.run(
        ['git', 'mktree', '--missing'],
        input=lines + '\n',
        capture_output=True, text=True, cwd=repo_path
    )
    return result.stdout.strip()

# Update a file in a subdirectory tree
# 1. Get parent directory tree
parent_entries = ls_tree('HEAD:src/components/admin', repo_path)

# 2. Remove old entry for the file being replaced
parent_entries = [(m, t, s, n) for m, t, s, n in parent_entries if n != 'TargetFile.tsx']

# 3. Append new blob
parent_entries.append(('100644', 'blob', blob_sha, 'TargetFile.tsx'))

# 4. Build the new subtree
new_subtree_sha = mktree(parent_entries, repo_path)

E. Verify no duplicate entries in every tree

count=$(git ls-tree <tree_sha> | wc -l)
unique=$(git ls-tree <tree_sha> | awk '{print $4}' | sort | uniq | wc -l)
echo "Entries: $count, Unique: $unique"
# Must be equal

F. Build the full root tree (walking up the directory hierarchy)

Repeat the ls_tree โ†’ filter โ†’ append โ†’ mktree pattern for each parent directory up to the root. Each level replaces the child entry with the newly created subtree SHA.

G. Create the commit

# Parent MUST be the last successfully pushed SHA โ€” hardcode it, never assume HEAD
PARENT_SHA = "abc123..."  # last pushed commit

result = subprocess.run(
    ['git', 'commit-tree', root_tree_sha, '-p', PARENT_SHA, '-m', 'feat: description'],
    capture_output=True, text=True, cwd=repo_path,
    env={**os.environ, 'GIT_AUTHOR_NAME': 'Name', 'GIT_AUTHOR_EMAIL': 'email',
         'GIT_COMMITTER_NAME': 'Name', 'GIT_COMMITTER_EMAIL': 'email'}
)
new_commit_sha = result.stdout.strip()
print(f"New commit: {new_commit_sha}")
๐Ÿšจ
Never use an intermediate failed commit as parent

If a previous attempt produced a commit with a bad tree, using it as the parent pulls bad objects into the push pack. GitHub will reject the push. Always use the last successfully pushed SHA.

H. Update the ref

with open('/path/to/repo/.git/refs/heads/main', 'w') as f:
    f.write(new_commit_sha + '\n')

I. Verify the push pack is clean

git rev-list --objects <new_commit_sha> ^<last_pushed_sha> | grep <any_suspected_bad_sha>
# Must return nothing (empty output = clean pack)

J. Push (from Windows terminal)

# Run from Windows PowerShell โ€” sandbox gets 403
git push origin <new_commit_sha>:refs/heads/main

6. Rollback

To revert to a previous deployment:

Via Vercel dashboard: 1. Go to Project โ†’ Deployments 2. Find the last working deployment 3. Click the three-dot menu โ†’ Promote to Production 4. Confirm. Traffic switches within seconds (no rebuild required)

Via git (if code fix is needed): 1. Identify the last good commit SHA 2. Create a revert commit: git revert <bad_commit_sha> 3. Push to main โ€” triggers a new production build


7. Preview Deployments

Every PR automatically gets a preview URL from Vercel:

https://sonan-digital-crm-<git-hash>.vercel.app

Preview deployments: - Use the same environment variables as production (Vercel copies them) - Are accessible only to team members with Vercel access (not public by default) - Are automatically cleaned up after the PR is closed


8. Production Promotion

If a branch is deployed as a preview and needs to be promoted:

  1. Merge the branch to main via a PR
  2. GitHub Actions deploy-production.yml triggers automatically
  3. Vercel rebuilds from main and sets the new deployment as production

Or, if the preview deployment is already verified: 1. Vercel Dashboard โ†’ Deployments โ†’ find the preview โ†’ Promote to Production


9. Vercel Cron Jobs

Cron jobs are configured in vercel.json:

{
  "crons": [
    {
      "path": "/api/admin/invoices/cron",
      "schedule": "0 8 * * *"
    },
    {
      "path": "/api/admin/appointments/cron",
      "schedule": "0 * * * *"
    }
  ]
}

Vercel Cron requires a Pro plan. Cron jobs appear under Project โ†’ Cron Jobs in the dashboard. Logs for each run appear in the Vercel function logs for that endpoint.


10. Post-Deployment Checklist

Run this checklist after every production deployment:

  • [ ] Vercel dashboard shows deployment status "Ready"
  • [ ] https://app.sonandigital.com returns HTTP 200
  • [ ] Admin login at /auth/login completes successfully (email + MFA)
  • [ ] MFA challenge works correctly
  • [ ] Portal login at /portal/login completes successfully
  • [ ] GET /api/admin/notifications returns 200 (not 500)
  • [ ] Check Vercel โ†’ Functions tab: all functions listed as Edge (not Lambda)
  • [ ] Check Sentry dashboard: no new crash events in the last 15 minutes
  • [ ] Check Vercel โ†’ Cron Jobs: crons are listed with correct schedules
  • [ ] Stripe webhook: verify Stripe dashboard shows recent successful webhook delivery
  • [ ] If this release included DB migrations: verify migration applied by checking table structure in Supabase Dashboard

Monitoring

This page covers error tracking, log management, cron job verification, uptime monitoring, and alerting for the SONAN DIGITAL CRM production environment.


1. Sentry โ€” Error Tracking

Sentry is the primary error tracking and performance monitoring tool. It captures unhandled exceptions and performance traces from both the client-side (browser) and server-side (edge functions).

Configuration

Variable Purpose
NEXT_PUBLIC_SENTRY_DSN Client-side SDK initialization (visible in browser โ€” safe)
SENTRY_AUTH_TOKEN CI/CD source map upload (org token with org:ci scope โ€” server only)

SDK initialization files

// sentry.client.config.ts โ€” runs in the browser
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,           // 10% of transactions traced
  replaysSessionSampleRate: 0.05,  // 5% of sessions replayed
  replaysOnErrorSampleRate: 1.0,   // 100% of error sessions replayed
  environment: process.env.NODE_ENV,
})
// sentry.server.config.ts โ€” runs in edge functions
import * as Sentry from '@sentry/nextjs'

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.05,  // 5% of server transactions traced
  environment: process.env.NODE_ENV,
})

Source map uploads

Source maps are uploaded to Sentry during the CI build step, enabling readable stack traces in production (which runs minified/compiled code). This requires SENTRY_AUTH_TOKEN with the org:ci scope.

# Automatically runs via next build with Sentry webpack plugin
# Configured in next.config.ts via withSentryConfig()

What Sentry captures

  • Unhandled JavaScript exceptions (client and server)
  • Failed API route handler errors (uncaught exceptions bubble to Sentry's Next.js instrumentation)
  • React component render errors (via Error Boundaries instrumented by Sentry)
  • Performance traces for page loads and API responses

What Sentry does NOT capture automatically

  • Handled errors returned as { error: "..." } JSON responses โ€” these are expected application behavior
  • Supabase query errors when the application explicitly handles them
  • Stripe webhook events (handled externally; check Stripe dashboard)

To manually capture a handled error for visibility:

import * as Sentry from '@sentry/nextjs'

try {
  // risky operation
} catch (err) {
  Sentry.captureException(err, {
    tags: { feature: 'invoice-cron' },
    extra: { tenantId: caller.tenantId },
  })
  // still return a user-facing error response
  return NextResponse.json({ error: 'Cron failed' }, { status: 500 })
}

2. Vercel Logs

Vercel provides two types of logs accessible from the dashboard:

Build logs

Available at: Project โ†’ Deployments โ†’ [deployment] โ†’ Build Logs

Use build logs to diagnose: - TypeScript compilation errors - Missing export const runtime = 'edge' on routes - Missing environment variables during build - next build output (page sizes, edge function sizes)

Function logs (runtime logs)

Available at: Project โ†’ Logs (real-time streaming) or Project โ†’ Deployments โ†’ [deployment] โ†’ Function Logs

Function logs capture: - console.log / console.error output from edge functions - Request metadata (path, status code, duration, region) - Cold start events

Important logging conventions:

// Good โ€” structured logging with context
console.log(JSON.stringify({
  event: 'invoice_cron_run',
  processed: count,
  overdue: overdueIds,
  tenantId: 'redacted',  // never log actual tenant IDs in production
  durationMs: Date.now() - start,
}))

// Bad โ€” log sensitive data
console.log('Processing tenant', caller.tenantId, 'user', caller.userId)

Log retention

Log type Retention (free/hobby tier) Retention (Pro tier)
Build logs 7 days 30 days
Function logs 1 day 7 days
Deployment logs Indefinite (per deployment) Indefinite
โ„น๏ธ
Log retention limitation

The 1-day function log retention on the free tier means you must check logs promptly after an incident. Consider upgrading to Pro for 7-day retention or exporting logs to an external service (e.g., Datadog, Logtail).


3. Cron Job Verification

Cron jobs run on Vercel's scheduler and are visible in Project โ†’ Cron Jobs in the dashboard.

Verifying a cron run completed

  1. Vercel Cron Jobs tab: Shows last run time and status (success/fail) for each configured cron.
  2. Vercel Function Logs: Filter by path (e.g., /api/admin/invoices/cron) to see the run's log output and response code.
  3. Database verification: After the overdue invoice cron, query the DB to confirm expected changes:
-- Verify overdue cron ran: check invoices that should have been updated
SELECT id, status, due_date, updated_at
FROM invoices
WHERE due_date < CURRENT_DATE
  AND status IN ('overdue', 'sent')
ORDER BY due_date ASC
LIMIT 20;

Manual cron trigger

To trigger a cron job manually (e.g., for testing or recovery after a missed run):

curl -X POST https://app.sonandigital.com/api/admin/invoices/cron \
  -H "Authorization: Bearer {{ CRON_SECRET }}" \
  -H "Content-Type: application/json"

Expected response:

{ "ok": true, "processed": 3 }

UptimeRobot provides external uptime monitoring โ€” it checks from outside Vercel's network and alerts on downtime even if Vercel's own dashboard shows the deployment as "Ready."

Monitor name URL Type Interval Alert
CRM App https://app.sonandigital.com HTTP(S) 5 min Email + SMS
Admin API Health https://app.sonandigital.com/api/health HTTP(S) 5 min Email
Portal Health https://app.sonandigital.com/portal HTTP(S) 5 min Email

Setup

  1. Register at uptimerobot.com
  2. Add monitors for the URLs above
  3. Set alert contacts to the engineering on-call email
  4. Configure keyword monitoring on /api/health to check for "ok":true in the response body

โ„น๏ธ
Not yet implemented in v1.0

Implementing a /api/health endpoint is recommended for the v1.1 milestone. The spec below is the target design.

A /api/health endpoint should return the operational status of the application and its dependencies:

// app/api/health/route.ts
export const runtime = 'edge'

export async function GET() {
  const checks: Record<string, 'ok' | 'error'> = {}

  // Check Supabase DB connectivity
  try {
    const supabase = createServiceClient()
    const { error } = await supabase.from('tenants').select('id').limit(1)
    checks.database = error ? 'error' : 'ok'
  } catch {
    checks.database = 'error'
  }

  const allOk = Object.values(checks).every(v => v === 'ok')

  return NextResponse.json({
    ok: allOk,
    version: process.env.NEXT_PUBLIC_APP_VERSION ?? 'unknown',
    checks,
    timestamp: new Date().toISOString(),
  }, {
    status: allOk ? 200 : 503,
  })
}

Expected response when healthy:

{
  "ok": true,
  "version": "1.2.0",
  "checks": {
    "database": "ok"
  },
  "timestamp": "2025-03-15T10:00:00.000Z"
}

6. Alerting

Sentry alert rules

Configure in Sentry โ†’ Project โ†’ Alerts:

Rule Condition Action
New issue Any new unresolved issue Email to engineering team
Error spike Error rate > 10 errors/minute Email + Slack notification
Performance regression p95 latency > 3000ms Email to engineering team
Unhandled crash level:fatal event Page on-call engineer immediately

Vercel error alerts

Vercel Pro tier provides email alerts for: - Build failures - Function error rate exceeding threshold

Configure at: Project โ†’ Settings โ†’ Notifications

  1. Sentry / Vercel alert fires โ†’ Email to engineering@sonandigital.com
  2. If unacknowledged after 15 minutes โ†’ SMS to on-call engineer
  3. If unresolved after 30 minutes โ†’ Escalate to team lead

7. Log Retention Summary

Source What it captures Retention
Sentry Exceptions, performance traces, replays 90 days (free tier)
Vercel Build Logs Compilation output, build errors 7โ€“30 days
Vercel Function Logs Runtime console output, request logs 1โ€“7 days
Supabase Logs DB query logs, auth events 7 days (free), 30 days (Pro)
Stripe Dashboard Webhook events, payment events 30 days
๐Ÿ’ก
Persistent log storage

For compliance or long-term debugging, consider integrating Vercel log drains to ship function logs to a persistent store such as Datadog, Logtail, or AWS CloudWatch. This is recommended before the platform onboards regulated clients.