Getting started

Production Deployment

Deploy OpsDash to Vercel (recommended), Docker, or any Next.js-compatible host.

Step 1 — Supabase Cloud

  1. Go to supabase.com/dashboard → New project
  2. Choose an organization, set name, database password, and region
  3. Wait for provisioning (~30 seconds)

Copy API keys

Settings → API → copy:

  • Project URLhttps://xxxxxxxxxxxx.supabase.co
  • anon public key
  • service_role key (click “Reveal”)

Push migrations

npx supabase login
npx supabase link --project-ref <your-project-ref>
npx supabase db push

project-ref is the subdomain portion of your Supabase URL — not the full URL.

Auth redirect URLs (required)

In Supabase Dashboard → Authentication → URL Configuration → Redirect URLs, add:

https://yourdomain.com/api/auth/callback
https://yourdomain.com/api/auth/confirm
http://localhost:3000/api/auth/callback
http://localhost:3000/api/auth/confirm

Step 2A — Vercel (Recommended)

git push origin main
# Import at https://vercel.com/new
# → Connect GitHub repo
# → Set all environment variables (see below)
# → Deploy

After deploying:

  • Update NEXT_PUBLIC_APP_URL to your production domain
  • Update Stripe webhook URL to https://yourdomain.com/api/webhooks/stripe
  • Update OAuth redirect URIs in Google/GitHub to your production Supabase URL

Vercel Cron configuration

Add to vercel.json at the project root:

{
  "crons": [
    { "path": "/api/cron/due-date-notifications", "schedule": "0 8 * * *" },
    { "path": "/api/cron/deals-at-risk-notifications", "schedule": "0 8 * * *" },
    { "path": "/api/cron/sla-breach-notifications", "schedule": "0 * * * *" },
    { "path": "/api/cron/digest-emails", "schedule": "0 * * * *" },
    { "path": "/api/cron/activity-reminders", "schedule": "*/15 * * * *" }
  ]
}

Vercel adds Authorization: Bearer {CRON_SECRET} automatically when CRON_SECRET is set.

Step 2B — Docker (Private Instance)

# On the target server
git clone <your-repo-url> /opt/opsdash
cd /opt/opsdash

# Create production env file
cp ".env copy.example" .env
# Fill in Supabase Cloud credentials, app URL, Stripe keys

# Build and start
docker-compose up -d --build

The docker-compose.yml runs the Next.js app on port 3000. Put Nginx or Caddy in front for SSL and domain routing.

Nginx reverse proxy

server {
    listen 443 ssl;
    server_name ops.clientname.com;

    ssl_certificate     /etc/letsencrypt/live/ops.clientname.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ops.clientname.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# Get SSL with Certbot
# certbot --nginx -d ops.clientname.com

See the full Private Instance Deployment Guide for client-specific setup steps.

Production Checklist

Supabase cloud project created, migrations applied (supabase db push)
All environment variables set on host
NEXT_PUBLIC_APP_URL set to production domain
Auth redirect URLs added in Supabase Dashboard
Stripe webhook configured for production URL
STRIPE_WEBHOOK_SECRET updated from production webhook
PLATFORM_ADMIN_EMAILS set
CRON_SECRET set and cron endpoints scheduled
VAPID keys set (if using push notifications)
Email confirmation configured in Supabase Dashboard → Authentication

Common Issues

“Organization not found” after signup

The handle_new_user() trigger may not have fired. Verify via Supabase Studio:

SELECT * FROM public.users WHERE id = '<your-user-uuid>';
SELECT * FROM public.organizations WHERE slug LIKE '%your-name%';

If empty: supabase db reset re-applies all migrations.

Stripe webhook returning 400

STRIPE_WEBHOOK_SECRET must match the signing secret from the webhook endpoint in Stripe Dashboard — not the publishable key.

Build errors after cloning

pnpm install
pnpm build

Requires pnpm (not npm or yarn) and Node.js ≥ 18.