Getting started
Configuration
All configuration lives in two places: .env (secrets and service keys) and quattora.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. Quattora <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 |
| QUATTORA_ANTHROPIC_API_KEY | Anthropic API key (optional — AI features; the AI master switch in quattora.config.ts must be on) |
| QUATTORA_OPENAI_API_KEY | OpenAI API key (optional — alternative AI provider) |
| QUATTORA_LICENSE_KEY | Signed JWT (RS256). Required when distribution is selfHosted or whitelabel |
| QUATTORA_LICENSE_PUBLIC_KEY | RS256 public key (PEM) to verify the license offline (built-in fallback) |
Stripe setup
1 — Create products and prices
The plan ladder and every amount live in packages/config/src/pricing/ (the catalogue and the versioned price book). Do not create products by hand — run the sync, which creates or renames products, creates prices with a stable lookup_key and EUR at parity, archives superseded prices and prints the env block:
node scripts/billing/sync-stripe-catalog.mjs # dry run — read the plan
node scripts/billing/sync-stripe-catalog.mjs --apply # against your TEST key first
# paste the printed NEXT_PUBLIC_STRIPE_PRICE_* block into .env, restart
node scripts/check-stripe-prices.mjs # proves Stripe charges what the book saysPrice ids follow NEXT_PUBLIC_STRIPE_PRICE_PLATE_{PLAN}_{INTERVAL} for plans, plus …_SEAT_*, …_ADDON_*, …_PARTNER_* and …_LICENCE_*. Re-pricing is a new price book version, never an edit to a live one — the sync archives the old prices and existing subscriptions keep billing on them.
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.
quattora.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 '@quattora/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 QUATTORA_LICENSE_KEY in .env over inlining.
key: process.env.QUATTORA_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.nameinquattora.config.ts - Replace
public/logo.svgandpublic/logo-icon.svg - Set
branding.accentColoras HSL values (e.g.221 83% 53%)
Full guide: Whitelabel configuration