Getting started
Configuration
All configuration lives in two places: .env (secrets and service keys) and opsdash.config.ts (feature overrides and branding).
Environment variables
Required
| Variable | Description | Source |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Supabase project URL | supabase start or Dashboard → Settings → API |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Public anon key | Same |
| SUPABASE_SERVICE_ROLE_KEY | Service role key — never expose to client | Same — click Reveal |
| NEXT_PUBLIC_APP_URL | App full URL, no trailing slash | http://localhost:3000 for dev |
Stripe (Billing)
| Variable | Description |
|---|---|
| STRIPE_SECRET_KEY | Stripe secret key (sk_test_... or sk_live_...) |
| NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe publishable key (pk_test_... or pk_live_...) |
| STRIPE_WEBHOOK_SECRET | Webhook signing secret (whsec_...) |
| STRIPE_PRICE_STUDIO_MONTHLY | Price ID for Studio plate, monthly |
| STRIPE_PRICE_STUDIO_YEARLY | Price ID for Studio plate, yearly |
| STRIPE_PRICE_GROWTH_MONTHLY | Price ID for Growth plate, monthly |
| STRIPE_PRICE_GROWTH_YEARLY | Price ID for Growth plate, yearly |
| STRIPE_PRICE_FULL_LOOP_MONTHLY | Price ID for Full Loop plate, monthly |
| STRIPE_PRICE_FULL_LOOP_YEARLY | Price ID for Full Loop plate, yearly |
| STRIPE_PRICE_AGENCY_MONTHLY | Price ID for Agency plate, monthly (sold via sales) |
| STRIPE_PRICE_AGENCY_YEARLY | Price ID for Agency plate, yearly |
Email (Resend)
| Variable | Description |
|---|---|
| RESEND_API_KEY | Resend API key — get at resend.com |
| RESEND_FROM | Optional sender address — e.g. OpsDash <noreply@yourdomain.com> |
If RESEND_API_KEY is not set, invitation emails are skipped silently. Invite links still work — share them manually.
Other variables
| Variable | Purpose |
|---|---|
| CRON_SECRET | Protects cron endpoints — openssl rand -hex 32 |
| PLATFORM_ADMIN_EMAILS | Comma-separated emails with platform admin access (case-sensitive) |
| VAPID_PUBLIC_KEY | VAPID public key for web push — npx web-push generate-vapid-keys |
| VAPID_PRIVATE_KEY | VAPID private key (server-side only) |
| NEXT_PUBLIC_VAPID_PUBLIC_KEY | Same as VAPID_PUBLIC_KEY (needed client-side) |
| GOOGLE_GMAIL_CLIENT_ID | Google OAuth client ID for Gmail sync |
| GOOGLE_GMAIL_CLIENT_SECRET | Google OAuth client secret |
| OPENAI_API_KEY | OpenAI API key (optional — AI features) |
| ANTHROPIC_API_KEY | Anthropic API key (optional — alternative AI provider) |
| OPSDASH_LICENSE_KEY | Signed JWT (RS256). Required when distribution is selfHosted or whitelabel |
| OPSDASH_LICENSE_PUBLIC_KEY | RS256 public key (PEM) to verify the license offline (built-in fallback) |
Stripe setup
1 — Create products and prices
The plate ladder lives in packages/config/src/plates.ts (the single source). Price IDs follow STRIPE_PRICE_{PLATE}_{INTERVAL}. In Stripe Dashboard → Products → Add product:
- Studio → Monthly
$24/month→STRIPE_PRICE_STUDIO_MONTHLY. Yearly$240/year→STRIPE_PRICE_STUDIO_YEARLY. - Growth → Monthly
$59/month→STRIPE_PRICE_GROWTH_MONTHLY. Yearly$590/year→STRIPE_PRICE_GROWTH_YEARLY. - Full Loop → Monthly
$99/month→STRIPE_PRICE_FULL_LOOP_MONTHLY. Yearly$990/year→STRIPE_PRICE_FULL_LOOP_YEARLY.
2 — Configure webhook
Stripe Dashboard → Developers → Webhooks → Add endpoint:
- URL:
https://yourdomain.com/api/webhooks/stripe - Events:
checkout.session.completed,invoice.payment_succeeded,customer.subscription.updated,customer.subscription.deleted - Copy the signing secret →
STRIPE_WEBHOOK_SECRET
For local testing:
stripe listen --forward-to localhost:3000/api/webhooks/stripeTest card: 4242 4242 4242 4242, any future date, any CVC.
opsdash.config.ts
For private instance deployments, this is the only file a buyer needs to edit. It overrides modules, features, branding, and billing without touching application code.
import { defineConfig } from '@opsdash/config';
export default defineConfig({
// Distribution channel: 'saas' | 'selfHosted' | 'whitelabel'.
// saas uses Stripe; selfHosted/whitelabel gate on the license planCeiling.
distribution: 'selfHosted',
branding: {
name: 'ClientOS',
logo: { full: '/logo.svg', icon: '/logo-icon.svg' },
accentColor: { light: '221 83% 53%', dark: '221 83% 60%' }, // HSL triplets, no hsl() wrapper
},
license: {
// Signed JWT (RS256). planCeiling is a plate id — it caps gating in
// selfHosted / whitelabel. Prefer OPSDASH_LICENSE_KEY in .env over inlining.
key: process.env.OPSDASH_LICENSE_KEY,
},
modules: {
crm: { enabled: true },
projects: { enabled: true },
automation: { enabled: false }, // disable entire module
invoicing: { enabled: true },
},
features: {
// minPlate is the single gating source. Ladder:
// free | studio | sales | growth | full_loop | agency
'crm:export': { minPlate: 'studio' },
},
auth: {
providers: ['email', 'google'],
},
nav: {
order: ['dashboard', 'crm', 'projects', 'forms', 'settings'],
},
});Branding
- Set
branding.nameinopsdash.config.ts - Replace
public/logo.svgandpublic/logo-icon.svg - Set
branding.accentColoras HSL values (e.g.221 83% 53%)
Full guide: Whitelabel configuration