The npm run init command launches an interactive wizard that configures your entire project. This script is located at scripts/init-project.js and handles everything from branding to database initialization.

How the Init Script Works

The initialization wizard is a Node.js script that uses readline for interactive prompts. It follows a sequential flow through multiple configuration steps, validating input and providing sensible defaults.

npm run init │ ├── Step 1: Branding (name, short name, description, URL) ├── Step 2: Business model (B2C/B2B) + onboarding toggle + OAuth providers (NEXT_PUBLIC_OAUTH_PROVIDERS) + magic-link OTP code length (OTP_LENGTH) ├── Step 2b: Billing model (subscription/license/hybrid) + default currency ├── Step 2c: Features (newsletter, blog, changelog, prelaunch mode, referral program, affiliates program, server-side error logs, CDN edge caching — each gated feature auto-generates its salt: REFERRAL_SALT / AFFILIATES_SALT / LOGS_SALT) ├── Step 3: Admin email ├── Step 4: Contact information (contact + support emails) ├── Step 5: Social links (Twitter, GitHub, LinkedIn, Discord) ├── Step 6: Legal information (company, address, registration) ├── Step 7: Email configuration (provider + sender + newsletter list) ├── Step 8: Integrations (Crisp, analytics, Stripe mode, PWA) │ ├── Step 9: Supabase configuration │ ├── Project URL │ ├── Publishable key │ ├── Secret key │ └── PostgreSQL connection URL │ ├── Step 10: Database initialization │ ├── Connect to PostgreSQL │ ├── Execute schema.sql │ ├── Execute cms-schema.sql │ ├── Set admin email in app_settings │ ├── Set jobs API URL in app_settings (warns + confirms if localhost vs hosted DB) │ ├── Enable pg_cron and pg_net extensions │ ├── Generate JOBS_SECRET_KEY (rejects placeholder values from .env) │ ├── Store JOBS_SECRET_KEY in Supabase Vault (idempotent: delete-then-create) │ ├── Purge orphan cron entries (cron.job rows with no matching public.jobs row) │ ├── Re-sync existing jobs so cron commands pick up new URL/secret │ └── Create CMS media storage bucket │ └── Step 11: Generate files ├── .env.local (environment variables) ├── config/app.ts (app configuration) ├── config/pricing.ts (update defaultCurrency) └── config/workspace.ts (B2B only)

The generated .env.local includes TRUST_CLOUDFLARE_IP=false by default. Leave it disabled unless the deployed origin is locked behind Cloudflare or a trusted proxy strips spoofed client-IP headers.

Step-by-Step Breakdown

Here is a detailed look at each step of the setup wizard and what it configures:

Step 1: Branding

Configure your app's identity:

Default Values

Press Enter to accept defaults shown in parentheses. The script validates URLs and email formats automatically.

Step 2: Business Model

Choose between B2C (individuals) and B2B (teams/organizations), toggle the onboarding flow, then configure auth-surface knobs:

  • OAuth providers (NEXT_PUBLIC_OAUTH_PROVIDERS) — comma-separated list of Supabase provider ids rendered as buttons on /login. Supported ids: apple, azure, bitbucket, discord, facebook, figma, github, gitlab, google, kakao, keycloak, linkedin, linkedin_oidc, notion, slack, slack_oidc, spotify, twitch, twitter. Each id must also be enabled in your Supabase Auth dashboard; empty value = magic link only.
  • OTP code length (OTP_LENGTH) — integer between 6 and 10 (default 6). MUST match your Supabase project's Authentication → Email OTP length setting; Supabase generates the code, this env var tells our UI/API how many digits to expect. Invalid values fall back to 6.
Model Description Use Case
B2C Personal accounts auto-created on signup. No workspaces or team features. Consumer apps, individual users
B2B Workspace accounts with invitations, roles, and team billing. SaaS for teams, enterprise apps

Step 2b: Billing Model & Currency

Choose how users pay for access:

Model Description Use Case
Subscription Recurring monthly/yearly payments for continued access. SaaS with ongoing value delivery
License One-shot payment (lifetime, yearly license, etc.). Tools, templates, downloadable products
Hybrid Both subscription and license options available to users. Flexible monetization strategies

You also choose a default currency for pricing display. The platform supports multi-currency (EUR, USD, GBP, CAD, CHF) with locale-based automatic selection, but this sets which currency is shown by default. The selected value is written to defaultCurrency in config/pricing.ts.

Currency Symbol Typical Use
EUR European markets (default)
USD $ US and international markets
GBP £ UK market
CAD $ Canadian market
CHF Fr. Swiss market

Step 2c: Features

Toggle optional platform features:

Feature Default Description
Newsletter signup Enabled Show newsletter subscription forms on landing page and footer. List management handled by the active email provider.
Blog section Disabled Enable the blog with CMS-driven posts, categories, and tags.
Changelog Enabled Public changelog page with admin CRUD. Multi-locale content, version badges, type badges. Accessible at /changelog and managed from /admin-dashboard/changelog.
Prelaunch mode Disabled When enabled: CTA buttons show a waitlist form, login button is hidden, buy buttons display "Coming soon", and sign-in is gated server-side by an admin allowlist (PRELAUNCH_ALLOWED_EMAILS, comma-separated). Magic-link API blocks non-allowlisted emails before generateLink; OAuth callback signs out + deletes the auto-created auth user. Set NEXT_PUBLIC_PRELAUNCH=false in .env.local when ready to launch.
Referral program Disabled Account-centric, trigger-based credit rewards. Enabling it auto-generates a REFERRAL_SALT (32-byte hex, used to hash visitor IP + User-Agent before storage) when one is not already set. Program config lives in config/referral.ts. Admin dashboard: /admin-dashboard/referrals. User dashboard: /private-dashboard/referrals.
Affiliates program Disabled Cash-commission partner program (Stripe Connect payouts), distinct from referrals. Auto-generates a separate AFFILIATES_SALT (distinct from REFERRAL_SALT) for click/attribution hashing. Tiers, commission rates, hold-period, and cookie window live in config/affiliates.ts. Admin: /admin-dashboard/affiliates; public landing: /[locale]/affiliates; user dashboard: /private-dashboard/affiliates.
Error logs Disabled Server-side error monitoring surfaced at /admin-dashboard/logs. Enabling it auto-generates a LOGS_SALT (used to hash client IPs) and prompts for log retention (default 90 days, clamped 1–365). Retention is enforced by the purge-error-logs cron job, which is seeded into the jobs table only when this gate is on at init time.
CDN edge caching Disabled Sets CDN_PUBLIC_CACHE_ENABLED (+ CDN_PUBLIC_S_MAXAGE / CDN_PUBLIC_SWR). When enabled, the middleware adds a cacheable Cache-Control only on anonymous GET requests to public content pages (home, /blog, /pricing, legal, /changelog, /affiliates, /contact) that carry no Supabase auth cookie and set no cookies; authenticated/dashboard/API responses stay private, no-store. The strict per-request CSP nonce is preserved. Requires a CDN configured to bypass cache on a session cookie (sb-*-auth-token) — see Production → Caching.

Step 3: Admin Email

Set the super admin account. This email will automatically receive is_admin=true in the database:

Important

Use a real email address you can access. This account can access /admin-dashboard and manage all users, subscriptions, and content.

Step 4: Contact Information

Provide your contact email (for the contact form submissions) and support email. These are used in the footer and contact page.

Step 5: Social Links

Optional social media links for footer and SEO:

Step 6: Legal Information

Required for Terms of Service and Privacy Policy pages:

Step 7: Email Provider

Choose your transactional email provider. The boilerplate ships with a provider-agnostic abstraction (lib/email) so swapping providers later is a config-only change.

  • brevo — Brevo (formerly Sendinblue). Default. Single API key.
  • mailjet — Mailjet Send API v3.1 + Contacts. Two keys (public + private).
  • noop — logs only, never delivers. Useful for dev / CI.

The wizard prompts only for the credentials of the provider you select.

Getting your API keys

Brevo: app.brevo.com/settings/keys/api

Mailjet: app.mailjet.com/account/apikeys (use both public and private keys)

Step 8: Integrations

Configure optional third-party services and runtime features:

  • SEO indexing (NEXT_PUBLIC_INDEXABLE) — allow search engines to crawl the site. When disabled, robots.txt returns Disallow: / globally. Leave off for staging/dev; flip to true for production launches.
  • Crisp live chat — Enable/disable and provide your Website ID
  • Analytics — Google Tag Manager ID, Meta Pixel ID, and X (Twitter) Pixel ID (Google Ads conversion IDs are set manually in .env.local)
  • Stripe mode — Choose between test mode (development) and live mode (production)
  • Trial period days (DEFAULT_TRIAL_PERIOD_DAYS) — default free-trial length in days for subscription plans that opt in via trialDays. Unset/blank → 7. 0 disables trials. Invalid values fall back to 7; values above 730 are clamped (Stripe max).
  • Trial credits (DEFAULT_TRIAL_CREDITS) — credits granted up-front when a subscription enters status='trialing' so the user can use the app during the trial. The first paid invoice tops up the difference (full plan allotment − trial amount). Capped at the plan's included credits. Unset/0 → trialing users get nothing until the trial converts.
  • PWA / Push notifications — Enable offline support and push notifications (auto-generates VAPID keys via web-push when missing)

Step 9: Supabase Configuration

Connect to your Supabase project:

Finding Supabase Credentials

In Supabase Dashboard: Settings → API for URL and keys. Settings → Database → Connection string → URI for PostgreSQL URL.

Step 10: Database Initialization

The script automatically initializes your database:

  1. Connects to PostgreSQL

    Uses the pg library to establish a connection to your Supabase database.

  2. Executes schema.sql

    Creates all core tables: profiles, accounts, roles, memberships, subscriptions, credits, chat sessions, etc.

  3. Executes cms-schema.sql

    Creates CMS tables: cms_pages, cms_blocks, blog_categories, blog_tags, blog_post_tags, and media storage configuration. CMS/blog migrations are mirrored into this file so a fresh reset also includes CMS data-normalization steps such as legacy fr/en JSONB key normalization to fr-FR/fr-CH/en-US/en-CA.

  4. Sets admin email

    Inserts your admin email into app_settings table so the first user with that email becomes super admin.

  5. Sets jobs API URL

    Configures jobs_api_url in app_settings to <your-url>/api/jobs/run so pg_cron can trigger scheduled jobs. If your NEXT_PUBLIC_APP_URL is localhost but the Supabase DB is hosted, the wizard warns and requires explicit yes confirmation — pg_cron runs on Supabase servers and cannot reach localhost, so writing it would silently break every scheduled job.

  6. Enables pg_cron and pg_net extensions

    Activates PostgreSQL extensions required for automatic background job scheduling and HTTP requests from the database.

  7. Generates JOBS_SECRET_KEY (rejects placeholders)

    Creates a cryptographically random 32-byte secret. If your .env already has a JOBS_SECRET_KEY, the wizard validates it: values matching common placeholder patterns (your-secret-key-here*, change-me, placeholder*, xxx+, single words like secret/password/test) or shorter than 16 characters are rejected and a fresh secret is generated instead.

  8. Stores JOBS_SECRET_KEY in Supabase Vault (idempotent)

    Securely stores the secret in Supabase Vault under the name jobs_secret_key, so pg_cron can authenticate when calling the jobs API. Re-running the wizard rotates the Vault entry (delete-then-create) instead of failing on the unique-key constraint, so changing your env var actually changes Vault.

  9. Purges orphan cron entries

    Drops cron.job rows that target /api/jobs/run but have no matching public.jobs row. These are typically left behind after deleting a job manually or rotating the schedule via a different code path. The query is anchored on command ILIKE 'select net.http_post(%' AND command LIKE '%/api/jobs/run%' and joined to public.jobs via LEFT JOIN ... WHERE j.id IS NULL, so cron entries from other extensions or unrelated user scripts are never touched. Cleanup is also exposed in the admin UI at /admin-dashboard/jobs/cron.

  10. Seeds the production job catalogue

    Seeds the production job handlers from lib/jobs/handlers.ts into the jobs table with sensible default cron expressions (e.g. cleanup-sessions daily at 2am, process-pending-emails every 5 minutes, process-account-deletions every 30 minutes). Always-on handlers are inserted unconditionally; the feature-gated handlers — purge-error-logs (gated on LOGS_ENABLED), check-license-expiration (gated on billingModel ∈ {license, hybrid}), and approve-mature-affiliate-conversions (gated on AFFILIATES_ENABLED) — are inserted only when their gate is on at init time. The two dev-only test fixtures are never seeded. All inserts use ON CONFLICT (name) DO NOTHING, so re-running the wizard never overwrites tuned cron expressions. All inserts use INSERT … ON CONFLICT (name) DO NOTHING, so re-running the wizard never overwrites admin-tuned cron expressions or config payloads. The AFTER INSERT trigger on public.jobs calls sync_pg_cron_job() for each new row, so they register with pg_cron immediately using the URL + Vault secret set in the steps above. Test fixtures (test-job, test-fail-job) are intentionally skipped.

  11. Re-syncs existing jobs

    Calls SELECT sync_pg_cron_job(id) FROM jobs WHERE is_enabled = true AND cron_expression IS NOT NULL. pg_cron bakes the URL and Bearer token into cron.job.command at schedule time, so jobs created before the wizard ran would otherwise keep stale values. This step refreshes them (the seed step above already covers newly inserted rows via the AFTER INSERT trigger).

  12. Creates CMS media storage bucket

    Creates a public cms-media bucket in Supabase Storage for CMS media uploads (images, files). Uses ON CONFLICT DO NOTHING so it's safe to re-run.

Idempotent Migrations

The SQL files use CREATE TABLE IF NOT EXISTS and ON CONFLICT DO UPDATE, so you can safely re-run the init script without data loss.

Step 11: File Generation

The wizard generates configuration files based on your inputs:

File Purpose Contents
.env.local Environment variables Supabase keys, Stripe keys, API keys, feature flags, prelaunch mode, and the fail-closed TRUST_CLOUDFLARE_IP=false rate-limit identity default
config/app.ts App configuration Branding, SEO, business model, features (newsletter, blog), legal info
config/pricing.ts Pricing configuration (partial update) Updates defaultCurrency to your selected currency (EUR, USD, GBP, CAD, or CHF)
config/workspace.ts Workspace settings (B2B only) Default roles, invitation settings, member limits

Running the Wizard Again

You can re-run npm run init at any time to update your configuration. The script will:

Detect existing .env.local
Pre-fill current values
Ask before overwriting
Skip DB init if already done

Manual Alternative

If the wizard fails or you prefer manual setup:

  1. Copy environment template

    cp .env.example .env.local

  2. Edit .env.local

    Fill in all required values manually (see Environment Setup section).

  3. Initialize database

    Go to Supabase Dashboard → SQL Editor → Run schema.sql then cms-schema.sql.

  4. Set admin email

    Run: UPDATE app_settings SET value = '[email protected]' WHERE key = 'admin_email';

Troubleshooting

Connection refused

Check that your PostgreSQL connection URL is correct and that your IP is allowed in Supabase's database settings (Settings → Database → Network).

Permission denied

Ensure you're using the secret key (not the publishable key) for database operations, or that your PostgreSQL user has CREATE permissions.

Schema already exists

This is safe to ignore. The scripts use IF NOT EXISTS clauses. Your existing data will not be affected.