SsuperslateDocs

Setup

Create a product repository and run the complete stack locally.

Setup

This is the supported path from delivered source to a custom local product. It requires Postgres but no third-party account or API key.

Pinned prerequisites

Use the repository-pinned versions, not whatever happens to be globally current:

ToolRequired versionRepository authority
Nodev24 (LTS line).node-version
Bun1.3.14.bun-version
pnpm11.21.0package.json
Vite+0.2.9package.json
Turborepo2.10.9package.json
PostgreSQL18docker-compose.yml

Docker Desktop or a compatible Docker engine is required for the supported local database. dbmate is installed in the server workspace and does not need a separate global install.

Verify the tools before scaffolding:

node --version
bun --version
pnpm --version
vp --version
docker version

Create a product repository

Released buyer path

Run the public bootstrapper and paste the licence key from your purchase receipt. It contains no paid source, and Node is the only prerequisite:

cd /path/to/parent
pnpm dlx superslate@latest paid-monitor
cd paid-monitor

bunx superslate@latest paid-monitor is equivalent. The CLI asks for your licence key, exchanges it for the newest release you are entitled to, downloads that release's archive, checksum, and manifest, verifies both records, applies the chosen scope and brand, installs the frozen dependency graph, and optionally initializes Git.

Pass the key non-interactively when scripting:

pnpm dlx superslate@latest paid-monitor --license-key SUPERSLATE-... --yes
SUPERSLATE_LICENSE_KEY=SUPERSLATE-... pnpm dlx superslate@latest paid-monitor --yes

Add --source-version 1.2.3 to reproduce a specific source release instead of taking the newest one. The key is sent in a request body and an Authorization header, never in a URL, and it is never written to a log.

Your repository access

Your purchase also grants read access to the private source repository. Connect GitHub in the customer portal to accept it. This is a perk for browsing source, watching releases, and opening issues — it is not required to create or update a product.

Pre-release and maintainer path

Keep the delivered source directory unchanged so it remains a clean upgrade/reference checkout. Run the local scaffold from its parent directory; the generated product must be a sibling, not a child of the template. This remains the supported path until the first commercial source tag and matching npm bootstrapper are published:

cd /path/to/parent
bun ./foundation-source/packages/create-app/src/cli.ts paid-monitor \
  --from ./foundation-source \
  --brand "Paid Monitor" \
  --git
cd paid-monitor

Replace the example directory and brand with the intended product values. The scaffold derives the internal workspace namespace from the brand (Paid Monitor becomes @paid-monitor); use the advanced --scope option only when an automation contract requires a different value. The namespace groups private packages and does not need to exist on npm. The scaffold:

  • copies only buyer-owned product files;
  • excludes the commercial landing app, private plans, historical Phase 2 notes, evaluation runs, build output, dependency directories, and source-control history;
  • rewrites the root package name and workspace scope;
  • replaces the source brand only on the counted brand manifest;
  • creates ignored root, server, web, and docs environment files;
  • generates a unique 32-byte Better Auth secret;
  • optionally creates an initial Git repository and commit;
  • automatically installs dependencies unless --no-install is selected;
  • offers to start Postgres and the core development servers immediately after creation.

It refuses a non-empty target. Do not point it at an existing project or run --from . while the target would be inside the source directory.

Install and start

Accepting the CLI's Start local development now? prompt runs the generated repository's canonical pnpm dev command. For non-interactive automation, pass --start; use --no-start when the command must finish after generation. --yes does not start a long-running development process unless --start is also present. Ctrl+C stops the complete application process tree, while the detached Postgres container remains available for the next run. Stop it with docker compose down from the generated directory.

From the generated repository, skip the install command when the CLI already completed it:

vp install --frozen-lockfile
pnpm dev

The root command starts and waits for the Compose Postgres service before launching any persistent task. The API then applies pending migrations before binding port 8000, while the web app binds port 5173. If preparation or either app fails, Turborepo stops every running development task before the command exits. This makes one Ctrl+C the normal shutdown path and prevents a failed API startup from leaving a hidden web, docs, or email process holding a port.

The separate documentation and React Email preview applications are optional during normal product development. Start all four applications with:

pnpm dev:all

This adds docs at http://localhost:3002/docs and React Email at http://localhost:3000. See Operate the documentation site before publishing it.

Focused package commands remain available for debugging or working on one surface. Start Postgres first when the API is involved:

docker compose up -d --wait postgres
pnpm dev:server
pnpm dev:web
pnpm dev:docs
pnpm dev:email

Compose scopes the database container to the generated project. The scaffold selects the first available port starting at 5432, records it in the ignored root .env, and writes the same port into apps/server/.env. The normal command therefore remains:

pnpm dev

Use the namespace derived during scaffolding. Open http://localhost:5173. The API listens on http://localhost:8000; the CLI completion summary reports the selected Postgres port.

The local database publishes to 127.0.0.1 only. Docker otherwise binds published ports on every interface and installs iptables rules ahead of host firewalls such as ufw, which would serve this container to any untrusted network the machine joins. Its app:app credentials are deliberately disposable and exist to keep local setup keyless; they are not a secret to protect and not a value to carry into a deployed environment. Every production path takes its own credentials — self-hosted Compose fails to start unless POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB are supplied, and the cloud roots read a managed DATABASE_URL secret. Do not remove the loopback prefix from docker-compose.yml to reach the database from another machine; forward the port over SSH instead.

The server must connect to Postgres, apply pending dbmate migrations, register event handlers, and preload all rendered email templates before it binds the port. Verify both operational endpoints:

curl --fail http://localhost:8000/health
curl --fail http://localhost:8000/ready

/health proves the process can answer. /ready proves the initialized database can answer. A successful health response with failed readiness is not a successful setup.

Zero-key behavior

With the generated environment files unchanged:

  • password signup, email verification, password reset, and magic-link sign-in work;
  • email bodies and action links are written to the local server log;
  • Google sign-in is disabled;
  • billing checkout and customer portal actions are disabled;
  • object-storage upload endpoints return a stable unavailable response;
  • Slack alerts and Sentry delivery are disabled.

Never use the email-log fallback in production. Production environment validation requires RESEND_API_KEY.

Optional integrations are atomic. Select one billing adapter and configure all of its required values, or leave billing unset. Configure every selected storage provider value together. Partial billing or storage configuration fails at startup.

Demo sign-in during local development

Repeated sign-in during development is avoidable. Seed a verified local account and enable a one-click button on /auth/login:

pnpm dev:server     # the seed script calls the running API
pnpm seed:demo      # creates demo@example.com and marks it verified

Then set both values in apps/web/.env:

VITE_DEV_LOGIN_EMAIL=demo@example.com
VITE_DEV_LOGIN_PASSWORD=demopassword123

The button signs in through the normal Better Auth password route; it adds no server endpoint and no authentication bypass. It renders only when import.meta.env.DEV is true, so vp build removes the control and both values from the bundle even when they are present in the build environment. pnpm seed:demo refuses any API origin outside localhost. Override the defaults with DEMO_EMAIL, DEMO_PASSWORD, or DEMO_API_URL.

Empty modules are hard to judge. Fill the docker-compose database with display data:

pnpm seed:demo-data   # display users, a file library, and an in-app notification inbox

The file library and the notification inbox are seeded for every account that can sign in locally, not only demo@example.com, so the modules are populated whichever seeded account you use. The script is idempotent and targets the docker-compose Postgres only. Notification rows carry metadata.seed = 'demo-inbox', so reseeding replaces them and leaves notifications the running application produced in place. Seeded file rows have no object behind them: downloads report the storage-disabled or missing-object state unless you also run:

pnpm --filter @app/server seed:demo-objects

This writes bytes for one account at a time (demo@example.com unless you pass another address).

First verification

Run the same deterministic path used by CI:

vp check
vp run -r typecheck
vp run -r test
vp run -r build

vp run -r test intentionally skips real Postgres integration unless both opt-in variables are set. Run the database suite against a disposable migrated database:

RUN_DB_INTEGRATION_TESTS=1 \
DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
pnpm --filter ./apps/server test

See Testing for isolation rules and Deployment for the compiled binary, Docker, Railway or Dokploy, Vercel, production email, and rollback proof.

Linting and formatting

vp lint is Oxlint and vp fmt is Oxfmt; vp check runs both plus the type-aware pass. Both read the lint and fmt blocks of the root vite.config.ts, which accept the full Oxlint and Oxfmt schemas. There is one repository-wide configuration and no per-package lint or format config.

Do not add .oxlintrc.json or .oxfmtrc.json. Vite+ does not auto-discover them, so the settings are silently ignored while appearing to be applied. Put the settings in vite.config.ts instead.

Oxlint enables only unicorn, typescript, and oxc by default, and setting lint.plugins replaces that base set rather than extending it. This repository enables react and jsx-a11y everywhere plus nextjs for apps/docs, so the list must be repeated in full in any override. react/rules-of-hooks ships in Oxlint's pedantic category, so it is turned on explicitly in lint.rules; enabling the react plugin alone would give you exhaustive-deps but not it.

Pinned-toolchain risk and escape hatches

Vite+ 0.x and TypeScript 7 are intentional early-adopter choices. Vite+ owns package-local tools and finite workspace verification. Pinned Turborepo owns only persistent root development tasks, where process groups, signal forwarding, forced escalation, and parent-death cleanup are required. Their APIs, plugin compatibility, and diagnostics can change faster than stable major releases.

Operational rules:

  • keep pnpm-lock.yaml, Node, Bun, pnpm, Vite+, and Turborepo pinned together;
  • do not accept automated major or beta upgrades without a fresh scaffold, CI, build, PWA, Sentry, and deployment proof;
  • reproduce a Vite+ failure with the owning package command before changing architecture;
  • reproduce a Turborepo development failure with the focused package commands before changing the process boundary;
  • retain package-native commands as the recovery path:
pnpm --filter ./apps/server test
pnpm --filter @app/server exec tsc -b --noEmit
pnpm --filter @app/web exec tsc -b --noEmit
pnpm --filter @app/web exec vitest run
pnpm --filter @app/server run build
(cd apps/web && pnpm dlx vite@8.1.5 build --mode production)

The final command is an online diagnostic escape hatch: it changes into the web workspace and runs the exact underlying official Vite version without changing the lockfile. Running it from the monorepo root is invalid because the root has no index.html. It has been verified against the current web config, but it does not replace the Vite+ release command or prove Vite+ orchestration. If it passes while vp build fails, restore the last green pinned Vite+ version/lockfile and diagnose the orchestrator instead of rewriting the application. Changing Vite+, Vite, TypeScript, Node, or Bun is a release-engineering change, not routine dependency maintenance.

Failure guide

SymptomCheck
vp install changes the lockfileUse pnpm 11.21.0 and --frozen-lockfile; do not accept the diff.
Server cannot connect to PostgresCompare DATABASE_URL with Compose and any POSTGRES_PORT override.
Postgres unreachable from another hostExpected; it binds 127.0.0.1. Forward the port over SSH instead.
/health works but /ready returns 503Database initialization, migration output, and Postgres health.
Server exits before bindingMissing migrations/templates, invalid environment, or failed DB.
No email arrives locallyRead structured server logs; local delivery does not call Resend.
Google, billing, upload, Slack, or Sentry offExpected when its complete optional configuration is absent.
Browser requests return 401API origin, CORS origin, Better Auth cookie, and credentials policy.
Generated code keeps source scope or brandRe-run scaffold tests; do not perform an unbounded text replacement.

Setup acceptance

Setup is complete only when:

  • the product is a generated sibling repository with its own unique auth secret and clean Git history;
  • frozen install, checks, tests, and builds pass without editing generated or lock files;
  • Postgres starts from the included Compose contract and /ready returns 200;
  • a password signup can be verified from the local email log;
  • a magic link works once and replay fails;
  • disabled integrations fail clearly without blocking basic local use.

On this page