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

VariableDescriptionSource
NEXT_PUBLIC_SUPABASE_URLSupabase project URLsupabase start or Dashboard → Settings → API
NEXT_PUBLIC_SUPABASE_ANON_KEYPublic anon keySame
SUPABASE_SERVICE_ROLE_KEYService role key — never expose to clientSame — click Reveal
NEXT_PUBLIC_APP_URLApp full URL, no trailing slashhttp://localhost:3000 for dev

Stripe (Billing)

VariableDescription
STRIPE_SECRET_KEYStripe secret key (sk_test_... or sk_live_...)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe publishable key (pk_test_... or pk_live_...)
STRIPE_WEBHOOK_SECRETWebhook signing secret (whsec_...)
STRIPE_PRICE_STUDIO_MONTHLYPrice ID for Studio plate, monthly
STRIPE_PRICE_STUDIO_YEARLYPrice ID for Studio plate, yearly
STRIPE_PRICE_GROWTH_MONTHLYPrice ID for Growth plate, monthly
STRIPE_PRICE_GROWTH_YEARLYPrice ID for Growth plate, yearly
STRIPE_PRICE_FULL_LOOP_MONTHLYPrice ID for Full Loop plate, monthly
STRIPE_PRICE_FULL_LOOP_YEARLYPrice ID for Full Loop plate, yearly
STRIPE_PRICE_AGENCY_MONTHLYPrice ID for Agency plate, monthly (sold via sales)
STRIPE_PRICE_AGENCY_YEARLYPrice ID for Agency plate, yearly

Email (Resend)

VariableDescription
RESEND_API_KEYResend API key — get at resend.com
RESEND_FROMOptional 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

VariablePurpose
CRON_SECRETProtects cron endpoints — openssl rand -hex 32
PLATFORM_ADMIN_EMAILSComma-separated emails with platform admin access (case-sensitive)
VAPID_PUBLIC_KEYVAPID public key for web push — npx web-push generate-vapid-keys
VAPID_PRIVATE_KEYVAPID private key (server-side only)
NEXT_PUBLIC_VAPID_PUBLIC_KEYSame as VAPID_PUBLIC_KEY (needed client-side)
GOOGLE_GMAIL_CLIENT_IDGoogle OAuth client ID for Gmail sync
GOOGLE_GMAIL_CLIENT_SECRETGoogle OAuth client secret
OPENAI_API_KEYOpenAI API key (optional — AI features)
ANTHROPIC_API_KEYAnthropic API key (optional — alternative AI provider)
OPSDASH_LICENSE_KEYSigned JWT (RS256). Required when distribution is selfHosted or whitelabel
OPSDASH_LICENSE_PUBLIC_KEYRS256 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:

  1. Studio → Monthly $24/month STRIPE_PRICE_STUDIO_MONTHLY. Yearly $240/year STRIPE_PRICE_STUDIO_YEARLY.
  2. Growth → Monthly $59/month STRIPE_PRICE_GROWTH_MONTHLY. Yearly $590/year STRIPE_PRICE_GROWTH_YEARLY.
  3. 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/stripe

Test 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

  1. Set branding.name in opsdash.config.ts
  2. Replace public/logo.svg and public/logo-icon.svg
  3. Set branding.accentColor as HSL values (e.g. 221 83% 53%)

Full guide: Whitelabel configuration

Configuration — OpsDash Docs | OpsDash