# supastack — Full Documentation Corpus > supastack is a modular foundation for AI SaaS products — a Next.js web app, > an Expo mobile app, and a Tauri desktop shell over shared TypeScript modules > (auth, workspaces, billing, AI, jobs, email, storage, observability). > Index: [llms.txt](https://supastack.dev/llms.txt) · Agent skill: [skill.md](https://supastack.dev/skill.md) --- Source: [Ship in 5 minutes](https://supastack.dev/docs/web/ship-in-5-minutes.md) # Ship in 5 minutes Run the Web demonstration on your computer with exact Windows and macOS commands, no paid provider credentials, and a clear path to real database-backed accounts. ## 1. Prepare your computer and open the project First complete Before you start: accept the GitHub invitation, install Node.js 22 or newer, Git, an editor and Corepack, then clone your Web repository or the All Platforms monorepo. Open the supastack folder in the editor and select Terminal → New Terminal. You should see package.json in the editor's top-level file list. All commands below run from this repository root. This starts the local demonstration without accounts at AI, billing or email providers and without Docker. Allow extra time for first-time dependency downloads. Follow only the Windows block or only the macOS/Linux block in the next steps; do not run both. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) [Or let an agent run the setup for you](https://supastack.dev/docs/web/agent-setup.md) ## 2. Create configuration files — macOS / Linux In your terminal, run each line below. The first copy creates .env for later migration commands. The second creates apps/web/.env.local, which the Web server actually reads. Both contain the example's local settings. If either destination file already exists, keep it and skip that copy command; do not overwrite saved settings. Windows users use the next section instead. corepack pnpm install downloads the libraries the app needs. If Corepack asks to download pnpm, answer y. Wait until it finishes and the terminal prompt returns. A final Done message without an error means you can continue. ``` cp .env.example .env cp .env.example apps/web/.env.local corepack pnpm install ``` ## 2. Create configuration files — Windows PowerShell Use these commands in PowerShell from the folder containing package.json. If .env or apps/web/.env.local already exists, skip its copy line and keep your settings. Wait for installation to finish before starting the server. If script execution is blocked, use corepack.cmd instead of corepack as explained in Before you start. ``` Copy-Item .env.example .env Copy-Item .env.example apps/web/.env.local corepack pnpm install ``` ## 3. Start the Web server — leave this terminal open Run the command below from the repository root. It starts only the Web app, so it works in both the Web-only repository and the All Platforms monorepo. Wait for the terminal to show Local: http://localhost:3000 and Ready. The first page may take a little longer to compile. The terminal should keep running; that is expected. In the All Platforms monorepo, corepack pnpm dev starts the available development workspaces together. If apps/docs is present, that separate docs app uses http://localhost:3001. The public guides you are reading are part of the Web app at http://localhost:3000/docs. You do not need port 3001 to follow this tutorial. ``` corepack pnpm --filter @supastack/web dev ``` ## 4. Open the app and complete one local task In your browser's address bar, open http://localhost:3000 to see the homepage. Then open http://localhost:3000/dashboard. Keep the prefilled name and email, or enter your own test values, and click Start local reference flow. This creates an in-memory demonstration workspace; it does not create a persistent account or send a real email. In the demonstration, open AI task from the sidebar, enter a short request such as Summarize the local acceptance criteria, and click Queue local task. When its status is queued, click Run local task, then View result. You should see completed output and an Email queued state. The AI response is deliberately fake and repeatable; the queued email is local. Restarting the server clears this demonstration data. To use /auth for real sign-up, first start Postgres and apply migrations using the Database and migrations guide. After that, sign-up or OTP messages from EMAIL_PROVIDER=local can be inspected at http://localhost:3000/mail-preview; they do not reach an external inbox. Mobile sign-in also uses this database-backed authentication, rather than the demonstration session. [Add Postgres and persistent accounts](https://supastack.dev/docs/web/database.md) ## 5. Stop, restart and fix common first-run problems Success means the homepage loads, the local workspace opens and a queued task reaches completed. When you finish, click the server terminal and press Ctrl + C. To resume later, reopen a terminal in the repository root and repeat the server command from step 3; you do not reinstall dependencies every time. If localhost:3000 does not open, check that the server terminal is still running and shows Ready. If port 3000 is in use, stop the other development server with Ctrl + C and retry. If you see a database error after opening /auth, return to /dashboard for the demonstration or complete the database guide. If the local demonstration is missing, check APP_ENV=local, AI_PROVIDER=fake, BILLING_PROVIDER=local, EMAIL_PROVIDER=local and JOB_PROVIDER=local in apps/web/.env.local, then restart. To customize the name and marketing text, open apps/web/src/marketing/site-config.ts in the editor. Follow Branding for the exact fields and verification steps. For the real account's appearance settings, complete database setup first, then open /settings/appearance. Automated acceptance commands take longer than this first-run walkthrough; find them under Workspace commands. [Fix a first-run error](https://supastack.dev/docs/web/troubleshooting.md) [Change your product's name and copy](https://supastack.dev/docs/web/marketing-site.md) --- Source: [Before you start](https://supastack.dev/docs/web/before-you-start.md) # Before you start Install Node.js, Git and an editor, accept your repository invitation, download the kit and check your computer is ready for the first run. ## 1. Choose what you want to run Use a Windows, macOS or Linux computer with an internet connection. A browser runs the Web app; a phone runs the Mobile app; the Desktop app opens in its own window. Your phone alone cannot perform the installation steps below. A repository is the folder of files you received through GitHub. Web, Mobile and Desktop purchases each provide the corresponding kit repository; the All Platforms bundle provides the monorepo, a folder containing all three apps. Accept the invitation for the repository you purchased. A Mobile or Desktop checkout still needs a reachable Web backend: the server that handles accounts and data. Those client repositories do not include the Web server. Prepare these tools once. Downloading tools and the first native compilation can take much longer than five minutes. The five-minute guides begin after these preparations; running locally does not publish your app to the internet. ## 2. Install Node.js Open the official Node.js download page below. Choose an LTS version numbered 22 or higher and the installer for your computer: .msi on Windows or .pkg on macOS. Open the downloaded installer, keep its defaults and finish installation. Node.js lets your computer run the app's JavaScript; npm is installed with it. Close and reopen your terminal after installing. On Windows open Start, type PowerShell and open it. On macOS press Command + Space, type Terminal and press Enter. On Linux open Terminal from your applications menu. Paste the command below, then press Enter. A result such as v22.x.x or v24.x.x confirms Node is available. If you see command not found or not recognized, reopen the terminal or reinstall Node with its PATH option enabled. [Official Node.js installers](https://nodejs.org/en/download) ``` node --version npm --version ``` ## 3. Install Git and a text editor Open the Git download page below, select your operating system and follow its installer. Git downloads your private repository and records file changes. On macOS, running the command below may offer Apple's command-line tools installation; accept it and rerun the command after it finishes. On Linux use the installation command shown for your distribution on the Git page. Install Visual Studio Code from its official download page. This is a text editor for opening the project and saving configuration files. Open it, choose File → Open Folder (File → Open on macOS), and later select the supastack folder you download. You do not need to write code to complete the first local demonstration. In a newly opened terminal, run the command below. A result beginning git version means Git is ready. If the command is missing, finish the installer and reopen the terminal. [Official Git installers](https://git-scm.com/downloads/) [Visual Studio Code download](https://code.visualstudio.com/download) ``` git --version ``` ## 4. Accept your GitHub invitation Sign in to GitHub in your browser using the account you supplied when purchasing. Open your GitHub invitation email, select View invitation and accept access. Then open the invited repository. You should see its files and a green Code button. A 404 usually means the invitation has not been accepted or you are signed in to a different GitHub account. Select Code → Local → HTTPS and copy the address shown. Use that exact address in the next step. Example addresses containing angle brackets are templates: replace them rather than pasting the brackets. You do not need to guess an organization name or make the private repository public. ## 5. Download the project to your computer Choose a folder for your projects, such as Documents. Open that folder in File Explorer or Finder. On Windows right-click an empty area and choose Open in Terminal, then choose PowerShell if needed. On macOS open Terminal, type cd followed by a space, drag the folder into the terminal and press Enter. The terminal is now working in that folder. In the first command below, replace YOUR_COPIED_HTTPS_ADDRESS with the HTTPS address you just copied. Run one line at a time. The first line creates a new folder named supastack; the second line enters it. If Git asks you to sign in, complete the browser sign-in. Do not use your GitHub account password as a Git password. If browser sign-in is unavailable, follow GitHub's HTTPS authentication guide linked below. Success: the supastack folder contains package.json, pnpm-workspace.yaml, apps and packages. This is the repository root. If Git says the destination already exists, use a different empty folder or open the existing clone; do not delete your work. Keep this terminal open for the next step. [GitHub HTTPS authentication help](https://docs.github.com/en/get-started/git-basics/about-remote-repositories) ``` git clone YOUR_COPIED_HTTPS_ADDRESS supastack cd supastack ``` ## 6. Check Corepack and the pinned package manager Run the first command below. Corepack selects the pnpm version recorded in package.json; pnpm downloads the libraries the project uses. If Corepack asks permission to download that version, type y and press Enter. This checkout pins pnpm 11.19.0; keep its pin rather than choosing a different package manager. If corepack is missing, run npm install --global corepack, reopen your terminal and retry. If PowerShell says an npm.ps1 or corepack.ps1 script cannot run, use npm.cmd or corepack.cmd for that command, including corepack.cmd pnpm install. You do not need to change your computer's execution policy. The filtered first-run commands call Corepack directly. If you use a Turbo command such as corepack pnpm dev, Turbo also needs a pnpm command on PATH. Windows setup below creates that command in a user-writable folder; its PATH change lasts for the current terminal only. Run this block again after opening a new terminal if pnpm is missing. On macOS/Linux, run corepack enable; if it reports a permission error, use the filtered first-run command instead. [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) ``` corepack --version corepack pnpm --version # Windows PowerShell only, for Turbo commands: $kitShimDir = Join-Path $env:LOCALAPPDATA 'supastack/corepack-shims' New-Item -ItemType Directory -Path $kitShimDir -Force | Out-Null corepack enable --install-directory $kitShimDir $env:PATH = "$kitShimDir;$env:PATH" ``` ## 7. Continue with your platform You are ready when Node and Git print versions, Corepack can select the pinned pnpm, and your terminal is in the folder containing package.json. Pick one tutorial below. Docker is optional for the Web demonstration; a database is required later for real accounts. Desktop also needs Rust and native build tools, which its tutorial explains before its launch command. [Let an agent run the setup for you](https://supastack.dev/docs/web/agent-setup.md) [Run the Web app](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Run the Mobile app](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Run the Desktop app](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) --- Source: [Commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) # Commands, files and configuration Learn where to paste commands, how to open the correct project folder, edit .env files, stop a server and report a setup error without sharing secrets. ## A command goes in the terminal A terminal is the window where you type instructions for your computer. In Visual Studio Code, first open your supastack folder using File → Open Folder, then select Terminal → New Terminal. The new terminal starts in that folder. On Windows choose PowerShell using the menu next to the terminal's + button. Use macOS/Linux blocks in a macOS or Linux terminal. Paste only the contents of a command block, one command at a time, and press Enter. Do not paste the surrounding Markdown backticks, a prompt such as PS C:\..., or example output. Lines starting with # are explanations; they do not run anything. Wait for each installation command to finish before entering the next command. A server command keeps running and does not return a fresh prompt until you stop it. ## Check which folder the terminal is using Repository root means the top-level supastack folder containing package.json, pnpm-workspace.yaml, apps and packages. Unless a step says otherwise, run commands there. The command below prints the current folder; the file listing should show package.json. In Windows PowerShell pwd and ls also work. If the listing shows only the contents of apps/web or apps/mobile, type cd ../.. and press Enter to return to the root. If it shows your home folder, type cd followed by a space and the full path to your clone, enclosed in quotation marks when it contains spaces. Opening the project folder in the editor and creating a new terminal is usually easier. ``` pwd ls ``` ## Configuration goes in a file, not in the terminal A block like AI_PROVIDER=fake is configuration text. Open the named .env file in the editor, find the line beginning AI_PROVIDER=, replace its value and save with Ctrl + S (Command + S on macOS). Each setting occupies one line in KEY=value form. Do not add Markdown backticks. Avoid duplicate copies of the same key in one file. Use root .env for database migration commands, apps/web/.env.local for the Next.js Web server, and apps/mobile/.env.local for Expo. Next and Expo run inside their own app folders and do not automatically load root .env. If a database connection changes, update DATABASE_URL in both root .env and apps/web/.env.local so migrations and the server use the same database. Keep the example file unchanged for future reference. Files whose names start with a dot may be hidden in your file manager; the editor's Explorer shows them. Make sure the filename is exactly .env.local, not .env.local.txt. These local files can contain passwords and are excluded from Git; do not upload them or paste their contents into support messages. [Find the meaning of each setting](https://supastack.dev/docs/web/project-structure.md) ## Keep the server running; use another terminal for commands The development server serves your app while you work. Leave its terminal open when a tutorial says open localhost:3000. localhost means this computer; :3000 is the port, like a numbered door used by the app. Put the URL into your browser's address bar, not the terminal. To run another command while the server is active, select Terminal → New Terminal in the editor. The new terminal is a separate window into the same project. To stop the server, select its terminal and press Ctrl + C once. On Windows, if asked Terminate batch job, type Y and press Enter. Wait for the normal prompt before starting it again. After editing Web environment settings, stop and restart the Web server with the command below. Saving a source file normally refreshes the page automatically, but environment changes need a restart. After editing Expo settings, stop Expo, then run corepack pnpm --filter @supastack/mobile start --clear to restart and clear its cached bundle. ``` corepack pnpm --filter @supastack/web dev ``` ## Read a failure and ask for useful help If an installation returns red error text, stop at that step. Read the first error rather than continuing to launch the app. Command not found usually means a missing tool; no package.json usually means the wrong folder; ECONNREFUSED usually means a required server or database is not running; EADDRINUSE means another process already uses the port. When asking for help, include your operating system, the guide URL and step number, the exact command you entered, and the first error message. Remove passwords, API keys, tokens and the password portion of any database URL. Do not send your .env files. A screenshot of the error can help, but copyable text is easier to diagnose. [Troubleshoot a specific setup failure](https://supastack.dev/docs/web/troubleshooting.md) --- Source: [Let an agent set it up](https://supastack.dev/docs/web/agent-setup.md) # Let an agent set it up Paste one ready-made prompt into a coding agent, let it run the Web setup on your computer, and check the report it writes about each step. ## You can delegate the boring part This repository was written so a coding agent can read it. It ships a map file (`llms.txt`), a rules file (`AGENTS.md`), a step-by-step runbook (`docs/agent-setup.md`) and a ready-made prompt (`prompts/agent-setup.md`). Instead of learning each setup command yourself, you paste one prompt and the agent follows the runbook on your computer. Delegating the typing does not mean skipping the checks. The agent works inside your copy of the repository and must report what it did with evidence. The sections below show what a good report looks like, so you stay in charge of the result. One honest limit: this automated path sets up the Web app and its backend — the server that handles accounts and data. The prompt and runbook ship with the Web repository and with the All Platforms monorepo. Mobile-only and Desktop-only checkouts carry their own scoped `llms.txt` and platform setup guides instead. [Set up the Mobile app locally](https://supastack.dev/docs/mobile/local-setup.md) [Run the Desktop app](https://supastack.dev/docs/desktop/run.md) ## What you need first Finish the preparation guide first: Node.js, Git and an editor installed, the GitHub invitation accepted, and the supastack folder downloaded to your computer. The agent works inside that folder, so everything in [Before you start](https://supastack.dev/docs/web/before-you-start.md) still applies. You also need a tool that can act as a coding agent — an editor with a built-in AI coding agent (an AI IDE), or an agent that runs in your terminal (an agent CLI). Any current agent-capable tool works; the prompt already spells out what it must read and run, so you do not need to write instructions yourself. Finally, you need one of two ways to run the app: Node.js 22 or newer installed on your computer — the path the preparation guide set up — or a container tool such as Docker or Podman, which runs the app inside containers and needs no installed Node.js. You pick one inside the prompt, and the agent follows it. ## Copy the prompt, fill in two blanks, paste Open `prompts/agent-setup.md` in the editor — it lives in the `prompts` folder at the top level of the supastack folder, next to `llms.txt`. Select its whole text and copy it. Two placeholders need your choices. Replace `` with one of docker, podman or host — host runs the app directly on your computer using the Node.js you installed; docker or podman runs it inside containers. Replace `` with where the app should end up — local-only runs it on this computer and deploys nothing, which is the right choice for a first look. Paste the edited text into the agent's chat and send it. The filled-in first line below shows the docker plus local-only choices; the numbered steps under that line stay exactly as the file ships. ``` Stand up this supastack checkout end to end for **docker** and deploy it to **local-only**. ``` ## What the agent does with the prompt The prompt points the agent at the runbook and the security rules, then tells it to work through them in order and prove each phase. In plain terms, the agent: - Reads the repository map (`llms.txt`), the project rules (`AGENTS.md`) and the credential-handling rules (`docs/agent-security.md`) before touching anything. - Creates the `.env` configuration files with `node scripts/setup-env.mjs`. The helper copies only from `.env.example`, so the agent never invents setting names, and it preserves secrets you already have instead of overwriting them. - Starts Postgres — the database the app keeps accounts and records in — and the app itself, as containers for the docker or podman path or with commands on your computer for the host path. - Runs the database migrations — the commands that create the app's tables — before the first real sign-in, because an account created before the tables exist is a broken one. - Verifies each phase at a checkpoint and finishes with a report marking every check PASS, FAIL, BLOCKED or NOT RUN, with the output that backs it up. ## What you check in the agent's report A good report shows evidence, not claims. Look for the real health response and command output behind each PASS — not a sentence like 'everything works'. The prompt tells the agent to stop and report after the same step fails twice — not to work around the failure — and to ask you before anything destructive, such as wiping a database. If your agent keeps improvising after repeated failures, stop it and take the first error to [the troubleshooting guide](https://supastack.dev/docs/web/troubleshooting.md). Two things must never appear: a security feature switched off to silence an error, and a secret value such as a password or key printed into the chat or the report. If you see either, treat the run as failed. Keep the demo and a real sign-in separate in your head. The `/dashboard` demo's local reference flow needs no database, so it can pass on its own; creating an account at `/auth` works only after migrations ran. 'The demo worked' is not yet 'accounts work'. ## If you would rather do it by hand Every step above also exists as a click-by-click tutorial. Run the [five-minute Web guide](https://supastack.dev/docs/web/ship-in-5-minutes.md) for the first demo, then [Web local setup](https://supastack.dev/docs/web/local-setup.md) for the full local environment with a real database. An agent is still useful after setup even when it did not do the installing — [the agent workflow guide](https://supastack.dev/docs/web/agents.md) covers everyday tasks such as new screens, restyling and reviews. --- Source: [Introduction](https://supastack.dev/docs/web.md) # Introduction What supastack is, what you get, and the path to a running app. ## Start here: what you have received The kit is a starting app you can customize. Web is the browser interface plus the server API; Mobile is the phone interface; Desktop is a native window displaying the Web app. An API is the server address a client calls to sign in or read data. Shared packages are reusable pieces used by these apps. Start with the Web local tutorial below. It tells you exactly how to download libraries, start the server and run one fake AI task. Then add the database for saved accounts, choose only the providers your product needs, customize your brand and prepare hosting. You can read the technical explanations later; they are not prerequisites for opening the first demo. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) [Follow the complete Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ## Build from a shared foundation supastack is a modular foundation for AI SaaS on web, mobile and desktop. Next.js carries the web UI and API, Expo provides native reference flows, and the Tauri desktop shell loads the same web app. Shared packages handle identity, workspaces, billing, AI, jobs, email, storage and observability. Start with the local reference flow to understand how the modules fit together. Configure production providers only after that flow works in your environment. ## Find your way around Applications compose the shared packages. Product-specific features belong in the consuming product codebase rather than in the reusable core. ``` apps/ web/ Next.js interface and API routes mobile/ Expo application packages/ auth/ Identity and sessions db/ Schema and workspace persistence billing/ Billing and entitlements ai/ Providers, routing, and usage jobs/ Background job contract email/ Transactional delivery ``` ## Choose your starting point Start with the five-minute local setup. Then follow the Continue setup link at the bottom of each guide: configure the database, authentication, plans and providers before preparing deployment. Use the complete documentation index to find every feature, provider, component and reference. The local golden path has passed in the project’s development environment. Timed clean-clone setup, real-provider smoke, deployment, and physical-device acceptance remain separate verification steps. [Set up locally in five minutes](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Browse all documentation](https://supastack.dev/docs/web/project-structure.md) [Mobile setup](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Desktop setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Learn how to navigate these docs](https://supastack.dev/docs/web/using-the-docs.md) ## Welcome to supastack supastack is a modular kit for AI SaaS products: a Next.js web app, an Expo mobile app, and a Tauri desktop shell, all built on shared TypeScript packages for auth, billing, AI, jobs, email, storage, and observability. After purchase you receive a GitHub invitation to the kit repositories — web, mobile, and desktop (the All Platforms bundle delivers the full monorepo). Clone the surface you're shipping and make it yours. These docs are a step-by-step path, not a reference dump: get the app running locally first, then walk the provider setup pages in order. Each page ends by pointing at the next one. ## Start a local server You need Node.js 22 or newer. The repo pins its package manager (packageManager: pnpm@11.19.0), so run pnpm through Corepack — no global install required. 1. Copy the environment file to create your local configuration. 2. Install the workspace dependencies. 3. Optionally start the local Postgres container for real persistence — the defaults run without it. 4. Start the dev server and open http://localhost:3000. ``` cp .env.example .env corepack pnpm install docker compose up -d postgres # optional local database corepack pnpm dev # → http://localhost:3000 ``` ## The .env file Environment settings choose the local or hosted service used by the kit. Copy .env.example to root .env for migrations and to apps/web/.env.local for the Web server. Expo uses apps/mobile/.env.local. Copying only root .env does not configure every app. The default fake/local providers need no paid API credentials. They let you complete the in-memory /dashboard demonstration; real sign-in at /auth still needs PostgreSQL and migrations. For a real provider, change only its named settings in the Web configuration file, save, then stop and restart the server. Never expose secrets through NEXT_PUBLIC_ or EXPO_PUBLIC_ values. ## Choose your path New here? Ship in 5 minutes takes you from clone to a running app on defaults alone. Building for a phone or desktop? The Mobile and Desktop tabs cover the Expo and Tauri surfaces. Once the app is up, the Set up providers group walks each integration doc by doc. --- Source: [Tech stack](https://supastack.dev/docs/web/tech-stack.md) # Tech stack The technologies behind the kit and the role each one plays. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Application stack The web app is a Next.js application composed from shared TypeScript packages — the same packages feed the Expo mobile app. Product chrome uses CSS modules over design tokens, so there is no runtime CSS-in-JS to debug, and object storage is S3-compatible. The tools below are the deliberate defaults. ## Next.js + React The web app runs [Next.js](https://nextjs.org) 16 with the App Router and [React](https://react.dev) 19 server components. - [Next.js](https://nextjs.org) - [React](https://react.dev) ## TypeScript + Turborepo [TypeScript](https://www.typescriptlang.org) strict mode applies across the whole pnpm workspace, and [Turborepo](https://turborepo.com) coordinates builds. - [TypeScript](https://www.typescriptlang.org) - [Turborepo](https://turborepo.com) ## Better Auth [Better Auth](https://better-auth.com) is the identity engine; an Auth.js engine is available for teams that standardize on it. - [Better Auth](https://better-auth.com) ## Drizzle + PostgreSQL Persistence runs through [Drizzle](https://orm.drizzle.team) ORM on [PostgreSQL](https://www.postgresql.org) — the schema and helpers every record flows through. - [Drizzle](https://orm.drizzle.team) - [PostgreSQL](https://www.postgresql.org) ## Billing [Stripe](https://stripe.com) billing is the default, and a [Lemon Squeezy](https://www.lemonsqueezy.com) adapter is included. - [Stripe](https://stripe.com) - [Lemon Squeezy](https://www.lemonsqueezy.com) ## Trigger.dev Background jobs run on [Trigger.dev](https://trigger.dev). - [Trigger.dev](https://trigger.dev) ## Transactional email Transactional email sends through [Resend](https://resend.com) or AWS SES. - [Resend](https://resend.com) ## Sentry + PostHog Observability runs through [Sentry](https://sentry.io) and [PostHog](https://posthog.com). - [Sentry](https://sentry.io) - [PostHog](https://posthog.com) ## Expo The [Expo](https://expo.dev) mobile app consumes the same TypeScript packages, so contract changes typecheck against both surfaces at once. Expo ships for iOS and Android. - [Expo](https://expo.dev) ## Shared modules Every cross-cutting capability lives in a package with a small contract: auth, db, billing, ai, jobs, email, storage, observability, config, i18n, and api. Applications compose them; product domain logic stays outside the kit. Each provider-backed module ships a fake or local adapter so the default test and development loop never calls a billable service. ## Verification tooling Vitest covers unit and integration behavior, Playwright covers browser flows, and ESLint plus Prettier keep the code consistent. The deterministic suite runs without provider credentials; real-provider acceptance is a separately gated path. ``` corepack pnpm test # deterministic unit + integration corepack pnpm check # lint, types, tests, format corepack pnpm build # production builds corepack pnpm test:e2e # browser flows ``` --- Source: [Local setup](https://supastack.dev/docs/web/local-setup.md) # Local setup Run the Web demonstration on your computer with exact Windows and macOS commands, no paid provider credentials, and a clear path to real database-backed accounts. ## 1. Prepare your computer and open the project First complete Before you start: accept the GitHub invitation, install Node.js 22 or newer, Git, an editor and Corepack, then clone your Web repository or the All Platforms monorepo. Open the supastack folder in the editor and select Terminal → New Terminal. You should see package.json in the editor's top-level file list. All commands below run from this repository root. This starts the local demonstration without accounts at AI, billing or email providers and without Docker. Allow extra time for first-time dependency downloads. Follow only the Windows block or only the macOS/Linux block in the next steps; do not run both. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) ## 2. Create configuration files — macOS / Linux In your terminal, run each line below. The first copy creates .env for later migration commands. The second creates apps/web/.env.local, which the Web server actually reads. Both contain the example's local settings. If either destination file already exists, keep it and skip that copy command; do not overwrite saved settings. Windows users use the next section instead. corepack pnpm install downloads the libraries the app needs. If Corepack asks to download pnpm, answer y. Wait until it finishes and the terminal prompt returns. A final Done message without an error means you can continue. ``` cp .env.example .env cp .env.example apps/web/.env.local corepack pnpm install ``` ## 2. Create configuration files — Windows PowerShell Use these commands in PowerShell from the folder containing package.json. If .env or apps/web/.env.local already exists, skip its copy line and keep your settings. Wait for installation to finish before starting the server. If script execution is blocked, use corepack.cmd instead of corepack as explained in Before you start. ``` Copy-Item .env.example .env Copy-Item .env.example apps/web/.env.local corepack pnpm install ``` ## 3. Start the Web server — leave this terminal open Run the command below from the repository root. It starts only the Web app, so it works in both the Web-only repository and the All Platforms monorepo. Wait for the terminal to show Local: http://localhost:3000 and Ready. The first page may take a little longer to compile. The terminal should keep running; that is expected. In the All Platforms monorepo, corepack pnpm dev starts the available development workspaces together. If apps/docs is present, that separate docs app uses http://localhost:3001. The public guides you are reading are part of the Web app at http://localhost:3000/docs. You do not need port 3001 to follow this tutorial. ``` corepack pnpm --filter @supastack/web dev ``` ## 4. Open the app and complete one local task In your browser's address bar, open http://localhost:3000 to see the homepage. Then open http://localhost:3000/dashboard. Keep the prefilled name and email, or enter your own test values, and click Start local reference flow. This creates an in-memory demonstration workspace; it does not create a persistent account or send a real email. In the demonstration, open AI task from the sidebar, enter a short request such as Summarize the local acceptance criteria, and click Queue local task. When its status is queued, click Run local task, then View result. You should see completed output and an Email queued state. The AI response is deliberately fake and repeatable; the queued email is local. Restarting the server clears this demonstration data. To use /auth for real sign-up, first start Postgres and apply migrations using the Database and migrations guide. After that, sign-up or OTP messages from EMAIL_PROVIDER=local can be inspected at http://localhost:3000/mail-preview; they do not reach an external inbox. Mobile sign-in also uses this database-backed authentication, rather than the demonstration session. [Add Postgres and persistent accounts](https://supastack.dev/docs/web/database.md) ## 5. Stop, restart and fix common first-run problems Success means the homepage loads, the local workspace opens and a queued task reaches completed. When you finish, click the server terminal and press Ctrl + C. To resume later, reopen a terminal in the repository root and repeat the server command from step 3; you do not reinstall dependencies every time. If localhost:3000 does not open, check that the server terminal is still running and shows Ready. If port 3000 is in use, stop the other development server with Ctrl + C and retry. If you see a database error after opening /auth, return to /dashboard for the demonstration or complete the database guide. If the local demonstration is missing, check APP_ENV=local, AI_PROVIDER=fake, BILLING_PROVIDER=local, EMAIL_PROVIDER=local and JOB_PROVIDER=local in apps/web/.env.local, then restart. To customize the name and marketing text, open apps/web/src/marketing/site-config.ts in the editor. Follow Branding for the exact fields and verification steps. For the real account's appearance settings, complete database setup first, then open /settings/appearance. Automated acceptance commands take longer than this first-run walkthrough; find them under Workspace commands. [Fix a first-run error](https://supastack.dev/docs/web/troubleshooting.md) [Change your product's name and copy](https://supastack.dev/docs/web/marketing-site.md) --- Source: [Project structure](https://supastack.dev/docs/web/project-structure.md) # Project structure Find the application, shared packages, and the boundaries between them. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open the root package.json in VS Code, then browse apps/web for the website, apps/mobile for Expo, apps/desktop for the desktop shell, and packages for shared pieces. 2. From the repository root run corepack pnpm --filter @supastack/web dev. 3. Open http://localhost:3000. Expected: the Web app starts from apps/web while shared packages remain in this same repository; the Desktop window wraps this Web app rather than containing a second product. The pnpm workspace contains Next.js in apps/web, Expo in apps/mobile, and shared packages under packages. Run workspace commands from the repository root. Application routes compose shared modules; product-specific behavior belongs in the consuming product. Read AGENTS.md and .ai/PROJECT_CONTEXT.md before changing behavior. File mirrors in .ai/ explain decisions and constraints that may not be apparent from a component alone. ``` apps/web/app/ Routes and layouts apps/web/src/ Web components and server composition apps/mobile/ Expo application packages/ Shared contracts and adapters tests/ Cross-module and browser checks docs/ Operational guides ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Where a change belongs Before editing, decide which app or package owns the change. The rule of thumb: if only one product surface needs it, put it in that app; if every surface needs it, put it in a shared package under `packages/`. The Web repository and the All Platforms monorepo contain every folder below. A standalone Mobile or Desktop checkout contains only its own app — the server code stays in the Web repository. [Build your first feature](https://supastack.dev/docs/web/first-feature.md) - A new marketing or landing page → `apps/web/app/` — the public website. - A new screen inside the signed-in product → `apps/web/app/` for the route plus `apps/web/src/` for its logic — pages compose the shared packages. - A new API route → `apps/web/app/api/` — the server half of the Web app. - A change every app shares — a database table, a contract, a provider adapter → the matching folder in `packages/`. - A phone-only screen → `apps/mobile/` — it talks to the Web app's API rather than holding its own backend. - A desktop-only behavior → `apps/desktop/` — the Tauri shell wraps the Web app and almost never forks product UI. ## Implementation workflow Start with the route that owns the user action, follow its server handler, and then inspect the shared package it calls. A UI component should not recreate workspace resolution, billing authorization, or provider selection. When adding a route, decide whether it is public or authenticated. Resolve authenticated ownership on the server before accessing user records, and keep server credentials outside client imports. ## Verification and troubleshooting If the command reports that no project matches the filter, check that the terminal is in the folder containing the root package.json. If port 3000 is already in use, stop the other local Web server first. Do not look for a separate Desktop chat or login implementation; the desktop shell loads the Web backend. Run the relevant tests and typecheck after changing a package contract. Inspect both web and mobile consumers when changing shared types. A successful web build alone does not establish compatibility with the native app. --- Source: [Configuration](https://supastack.dev/docs/web/configuration.md) # Configuration Keep application configuration explicit and provider credentials on the server. ## Make one configuration change and check it Start with a running Web app. In the editor open apps/web/.env.local; if it does not exist, copy .env.example there using the Web tutorial. For a harmless first check, find EMAIL_PROVIDER=local and leave it local. This setting sends product email to the local preview instead of a real inbox. Do not paste KEY=value lines into the terminal. For each service you enable, follow that provider's setup guide, replace only the named values, save the file, stop the Web server with Ctrl + C and restart with the command below. If validation names a missing key, reopen this file and compare that key with the provider guide. Keep root .env synchronized only for values used by database migrations; copying it alone does not configure Next.js. [Look up a configuration key](https://supastack.dev/docs/web/project-structure.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) ``` corepack pnpm --filter @supastack/web dev ``` ## The settings you will actually edit first `.env.example` at the repository root is the complete inventory of settings the kit understands; a name absent from that file does not exist. You only touch a handful at the start — the rest stay on their defaults until a provider guide asks for them. Which file each app reads: root `.env` feeds the migration commands, `apps/web/.env.local` feeds the Next.js server, and `apps/mobile/.env.local` feeds Expo. Editing one file does not configure the others, so `DATABASE_URL` lives in both root `.env` and `apps/web/.env.local`. - `APP_URL` — the address the app lives at; `http://localhost:3000` locally, your domain in production. - `DATABASE_URL` — the Postgres connection string migrations and the server share. - `AUTH_SECRET` and `CREDENTIAL_ENCRYPTION_KEY` — generated secrets; `node scripts/setup-env.mjs` fills them for you (that helper ships in the Web repository and the All Platforms monorepo). - The provider selectors — `EMAIL_PROVIDER`, `AI_PROVIDER`, `BILLING_PROVIDER`, `JOB_PROVIDER`, `CAPTCHA_PROVIDER` — each picks which adapter runs; the `local`/`fake`/`off` defaults need no account. - Provider secrets such as `RESEND_API_KEY` or `STRIPE_SECRET_KEY` — add only when the matching provider guide tells you to. ## Start with the environment example The root .env.example is the configuration inventory. Shared validation lives in packages/config. Review the selected application environment and provider modes together; a local interface is not a production credential strategy. The credential-free reference mode requires local/test application configuration and fake/local providers. Selecting a production provider changes the runtime requirements. ## Keep secrets behind the API Authentication secrets, database credentials, provider keys, Stripe secrets, and Trigger credentials are server-only. Never prefix them with NEXT_PUBLIC_ or EXPO_PUBLIC_. Do not print them during setup or include them in error responses. The browser receives public configuration and allowed model aliases. Workspace ownership, entitlement decisions, budgets, and provider selection remain server-enforced. ## Check a change before using it Validate configuration through the existing config package and run the relevant deterministic tests. Use the separately opted-in provider smoke path when you are ready to verify real integrations. ``` corepack pnpm test corepack pnpm check corepack pnpm build ``` --- Source: [Build your first feature](https://supastack.dev/docs/web/first-feature.md) # Build your first feature An end-to-end walk from schema to shipped screen. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Open VS Code at the repository root and ask your coding agent to create `apps/web/app/reports/page.tsx` and `apps/web/src/reports.ts`. Give it the report fields and allowed roles; require server-side membership checks and a query filtered by `workspaceId`. 2. Open `/reports` as a signed-in member and confirm only that workspace's records appear. 3. Try a signed-out and a second-workspace account; if either can read the records, stop and fix the server check. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Create the route Add a page beneath `apps/web/app/`. Start from the dashboard or content route so you reuse its session and active-workspace resolution. A route exists when its folder contains `page.tsx`; use a Server Component for protected reads. ``` apps/web/app/reports/page.tsx apps/web/src/reports.ts .ai/apps/web/app/reports/page.md .ai/apps/web/src/reports.md ``` ## Resolve ownership on the server Read the signed-in identity and active workspace through the existing auth/workspace helpers. Verify membership before loading records. Filter every query by the resolved `workspaceId`; never take an owner identifier from a form or model output. Check owner/admin permissions for management actions. When a feature is paid, check its entitlement at the read or action boundary before returning protected data; a hidden button does not enforce access. ## Verify access and recovery Test anonymous access, a valid member, a member of another workspace, a revoked session, and the required role. Add empty and failed-load states with a recovery action. Keep the route out of the public sitemap and add its prefix to robots disallows. [Workspace isolation](https://supastack.dev/docs/web/security.md) [Entitlement checks](https://supastack.dev/docs/web/setup-payments.md) ## Model the record Every product record starts in the database schema. Add a table in packages/db with a workspaceId column, generate the migration, and write the repository helpers that resolve the workspace before reading or writing. ``` // packages/db/src/schema.ts export const ideas = pgTable("idea", { id: uuid().primaryKey(), workspaceId: uuid().notNull().references(() => workspaces.id), title: text().notNull(), createdAt: timestamp().defaultNow(), }); corepack pnpm --filter @supastack/db db:generate ``` ## Authorize the action Create a server action that resolves the session, the workspace, and the entitlement in that order. Validate input with a zod schema, run the mutation through the repository, and return a typed result the interface can render. The order matters: a mutation that writes before checking workspace ownership has already crossed the boundary. Derive ownership on the server — never trust a workspace id sent by the browser. ## Compose the surface Build the page from shared components and tokens — the kit's blocks and ui-web primitives cover the common patterns. Handle loading, error, and empty states; a feature that only renders its happy path is not done. --- Source: [Build with a coding agent](https://supastack.dev/docs/web/agents.md) # Build with a coding agent Hand tasks to a coding agent: the files it reads, the pasteable prompt library, unattended setup, and the review that keeps you in control. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this repository at its root in your editor. A coding agent is an AI tool — Cursor, Claude Code, Devin or a similar assistant — that reads your project, edits files and runs commands. Everything on this page applies to whichever one you use. Quick terms: a prompt is the instruction text you paste into the agent. A diff is the list of file changes it proposes. The repository root is the top folder containing package.json — the agent should always start there. 1. Open the `prompts/` folder in the repository root and copy the prompt closest to your task. `new-landing-page.md` is a good first try. 2. Paste it into your agent and fill the ``: the route you want, what the page or screen should do, who may use it, and what success looks like. Then add one line: Read `llms.txt`, then `AGENTS.md`, then `.ai/PROJECT_CONTEXT.md` before touching code. 3. Let the agent list the files it will change and the checks it will run before it edits. When it finishes, review the diff — you are approving real changes to your product. 4. Publishing, deploying and paid provider calls are never part of a task prompt. Keep them as separate steps you approve explicitly. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) ## The files your agent reads The repository ships a small stack of files written for agents, not only for humans. You never paste this context by hand — point the agent at `llms.txt` and it reads the rest itself. Read order matters: `llms.txt` is the map, `AGENTS.md` sets the boundaries, and `.ai/PROJECT_CONTEXT.md` carries what earlier sessions learned. An agent that skips the boundaries produces code you cannot ship. | File | What it gives the agent | |---|---| | llms.txt | The repository entry map — every doc worth reading plus the security protocols, in read order | | AGENTS.md | The non-negotiable boundaries: local work only until you approve more, workspaceId scoping, server-only secrets | | .ai/PROJECT_CONTEXT.md | Cross-session memory — what changed recently and why; read before code changes | | .ai/ mirror docs | A matching .ai/.md note beside important files, recording per-file decisions and open threads | | docs/agent-setup.md | The unattended runbook — install, migrate and deploy phases with a verification checkpoint after each | | prompts/ | Pasteable task prompts for the jobs owners ask for most | | docs/agent-security.md | The acceptance checklist — authentication, workspace and RLS checks, redacted logs, deployment evidence | | .env.example | The complete environment inventory — the agent never invents a variable name absent from this file | ## The prompt library `prompts/README.md` is the index. Each file is a complete brief: paste it, fill the ``, and the kit's rules — design tokens over hardcoded colors, kit blocks over invented patterns — travel with it. For the zero-to-running path, paste `prompts/agent-setup.md` instead — it drives the unattended setup described next. [Let the agent run the setup for you](https://supastack.dev/docs/web/agent-setup.md) - `agent-setup.md` — stand the kit up and deploy it with an agent - `pick-a-style.md` — apply a design style across the whole product - `new-landing-page.md` — scaffold a marketing page from kit blocks - `new-app-screen.md` — add a screen inside the workspace app shell - `restyle-product.md` — re-theme an existing page onto kit tokens - `ui-audit.md` — run the visual and accessibility audit on a route ## Letting the agent install and deploy The kit can largely set itself up. Pasting `prompts/agent-setup.md` makes the agent execute `docs/agent-setup.md` — the shipped runbook — phase by phase: preflight checks, environment files, install, database start, migrations, app start, and optionally a VPS deploy. After each phase it runs that phase's verification checkpoint and reports real output, not a claim. Two helpers keep that safe. `scripts/setup-env.mjs` bootstraps the env files from `.env.example`: it creates only missing files, generates only missing or placeholder secrets, and its `--check` mode validates without writing. `docker-compose.dev.yml` is a fully containerized dev environment — Postgres plus a Node 22 dev container — so the agent does not need Node.js installed on your computer. Which repository you received matters here. The Web repository and the All Platforms monorepo ship the runbook, the compose files, `scripts/setup-env.mjs` and `prompts/`. The standalone Mobile and Desktop repositories ship a scoped `llms.txt`, `AGENTS.md`, the `.ai/` mirror docs and curated docs including `docs/agent-security.md` — but not the setup runbook or compose files, because the backend those apps talk to lives in the Web repository. [Walk the agent-driven setup](https://supastack.dev/docs/web/agent-setup.md) ## How the agent remembers between sessions Agents have no memory between sessions — the `.ai/` folder is the memory. `.ai/PROJECT_CONTEXT.md` records what changed recently and why. Beside it, a mirror doc such as `.ai/apps/web/src/reports.md` explains the decisions behind `apps/web/src/reports.ts` that the code alone does not show. The contract is simple: read a file's mirror before editing it, and update the mirror when the change lands. Open Threads entries record unfinished work so the next session resumes instead of starting over. A stale mirror is worse than none — when you review the agent's diff, check that the mirrors of changed files moved with it. ## The running app's own agent surface Your deployed product answers agents too — these endpoints exist on the running app, not only in the repository. - `GET /llms.txt` — the app serves its own llmstxt.org index: the guides, public pages and public endpoints, with absolute URLs. - `POST /api/mcp` — a stateless MCP endpoint exposing the app's tools. It accepts `sk_` API keys as bearer tokens only — browser cookie sessions never apply. Create keys under Settings → Developers. - `GET /api/v1/openapi` — the OpenAPI specification of the public REST API, for client generators and agent tools. - `GET /api/health` — the liveness and dependency probe that load balancers and uptime checks call; it reports provider configuration kinds, never secrets. - `/status` — a human-readable page reporting each subsystem's health. ## Review the agent's work honestly A finished task is evidence, not a summary. Before you keep the agent's work, check it the way `docs/agent-security.md` checks a deployment: When something is wrong, send the diff back — the agent fixes its own work fastest when you name the failing check. For anything it should never have attempted alone, say so explicitly once; the boundary holds for the rest of the session. [Verification gates and the test suite](https://supastack.dev/docs/web/testing.md) - It ran the tests it claims — ask for the command and its output, not a conclusion. - Secrets stayed server-only — nothing new carries a `NEXT_PUBLIC_` or `EXPO_PUBLIC_` prefix, and no key was printed into a report or commit. - Every user-owned read and write stayed scoped by `workspaceId` — no query trusts an id sent by the browser. - The `.ai/` mirror of each changed file moved with the code. - Publishing, deploying and paid provider calls stayed separate steps you approved — the agent never merged, pushed or deployed on its own. - If a step failed twice, it stopped and reported the failing command instead of improvising around it. --- Source: [Create your external accounts](https://supastack.dev/docs/web/external-accounts.md) # Create your external accounts The master checklist: which outside services the kit can use, what each one gives you, and the order to create them in. ## You can build before you buy Every feature in this kit first runs with a local stand-in — a built-in adapter that imitates the real service without an account. `EMAIL_PROVIDER=local` writes messages to a preview page instead of sending them, `AI_PROVIDER=fake` returns a deterministic fake answer, `BILLING_PROVIDER=local` completes checkout without a payment provider, `JOB_PROVIDER=local` queues background jobs in memory without running them, and `CAPTCHA_PROVIDER=off` keeps public forms open. The `.env.example` configuration ships this way, so the five-minute tutorial needs zero paid accounts. Add a real external account only when you ship the feature it powers: a database before real sign-in, an email provider before messages must reach inboxes, a billing provider before charging money. Each guide below names the console page, the values to copy, and where to paste them. [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/web/aws-ses.md) ## The complete account checklist This table lists every outside service a supastack product can need. Skip any row whose feature you are not shipping — a web-only starter never touches the mobile and desktop rows. The Guide column names the doc that walks that console click-by-click; the actual links are in Where to start at the end of this page. | Service | What it gives you | Needed for | Guide | |---|---|---|---| | Postgres database (Supabase, Neon, or Docker) | Tables for accounts, workspaces, and data | Real sign-in and any saved data | Set up database | | Stripe | Card payments and subscriptions on the web | Charging money in the web app | Add Stripe payments | | Resend / Amazon SES | Transactional email delivery | Sign-in codes and receipts reaching real inboxes | Add Resend email / Set up Amazon SES email | | Google Cloud OAuth client | The Continue with Google button | Offering Google sign-in | Add Google sign-in | | GitHub OAuth App | The Continue with GitHub button | Offering GitHub sign-in | Add GitHub sign-in | | AI provider key (OpenAI, Anthropic, or OpenRouter) | Real model answers | Live AI tasks instead of the fake adapter | Collect the remaining provider keys | | Trigger.dev | Managed background-job runs | Production background work | Collect the remaining provider keys | | S3-compatible bucket (e.g. Cloudflare R2) | File and object storage | Uploads and stored files | Collect the remaining provider keys | | Sentry or PostHog | Error tracking and product analytics | Observability in production | Collect the remaining provider keys | | Cloudflare Turnstile | Spam checks on public forms | Public sign-up and contact forms | Collect the remaining provider keys | | Domain registrar + DNS | Your own domain name | A public https://yourdomain.com address | Collect the remaining provider keys / Deploy to Google Cloud | | Hosting (Google Cloud Run or a VPS + Coolify) | A public HTTPS URL for the web app | Going live for real users | Deploy to Google Cloud / Deployment checklist | | Expo + EAS | Cloud builds of the native app | Anything past the Expo Go preview | Create mobile accounts | | Apple Developer Program | iOS signing, App Store submission, Sign in with Apple | iPhone releases and Apple sign-in | Create mobile accounts / Add Apple sign-in | | Google Play Console | The Android store listing | Play Store submission | Create mobile accounts | | RevenueCat | Verified native purchases and restores | Selling subscriptions inside the app | Set up RevenueCat purchases | | Apple Developer ID + signing CA | Signed and notarized desktop installers | Public desktop releases without warnings | Create desktop release accounts | ## A sensible creation order Create accounts in the order features come online. For most web products that means: 1. Postgres database, 2. transactional email, 3. Google and/or GitHub sign-in, 4. billing, 5. an AI provider — then storage, background jobs, observability, and spam protection as the product needs them. Hosting and a domain come last — a public URL is only useful once a working app exists to point it at. Mobile accounts (Expo, Apple, Google Play, RevenueCat) and desktop signing accounts matter only when you reach those platforms; skip them entirely for a web-only launch. Keep every secret — OAuth client secrets, API keys, webhook signing secrets, and the password inside `DATABASE_URL` — in `.env.local` files or your host's secret manager. Never place them in `NEXT_PUBLIC_` or `EXPO_PUBLIC_` variables, and never commit them to Git. ## Where to start The detailed guides below walk each console click-by-click. Work top to bottom: the sign-in providers, then the account walkthroughs for payments, email, and the remaining provider keys, then the provider setup references for the services your product ships, then deployment; the platform account guides when you reach mobile or desktop. [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/web/apple-sign-in.md) [Add Stripe payments](https://supastack.dev/docs/web/stripe-payments.md) [Add Resend email](https://supastack.dev/docs/web/resend-email.md) [Collect the remaining provider keys](https://supastack.dev/docs/web/provider-keys.md) [Set up database](https://supastack.dev/docs/web/setup-database.md) [Set up payments](https://supastack.dev/docs/web/setup-payments.md) [Set up email](https://supastack.dev/docs/web/setup-email.md) [Set up OAuth sign-in](https://supastack.dev/docs/web/setup-oauth.md) [Set up AI](https://supastack.dev/docs/web/setup-ai.md) [Set up background jobs](https://supastack.dev/docs/web/setup-jobs.md) [Set up file storage](https://supastack.dev/docs/web/setup-storage.md) [Set up observability](https://supastack.dev/docs/web/setup-observability.md) [Spam protection](https://supastack.dev/docs/web/captcha.md) [Set up custom domains](https://supastack.dev/docs/web/setup-domains.md) [Deploy to Google Cloud](https://supastack.dev/docs/web/deploy-google-cloud.md) [Deployment checklist](https://supastack.dev/docs/web/deployment.md) [Create mobile accounts](https://supastack.dev/docs/mobile/external-accounts.md) [Create desktop release accounts](https://supastack.dev/docs/desktop/external-accounts.md) [Connect the mobile project to EAS](https://supastack.dev/docs/mobile/eas-setup.md) [Set up RevenueCat purchases](https://supastack.dev/docs/mobile/revenuecat.md) [Sign desktop installers](https://supastack.dev/docs/desktop/signing.md) --- Source: [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) # Add Google sign-in Create a free Google Cloud OAuth client and wire Continue with Google into the app. ## Beginner walkthrough Prerequisites: the web app running locally (Ship in 5 minutes) and a migrated database for real accounts (Set up database). You need an ordinary Google account; everything here is free and takes about 10 minutes. Quick terms: OAuth is the standard where Google signs the user in on Google's page and hands your app a proof. A Google Cloud project is a container in Google's console that holds your app's credentials. The OAuth consent screen is the page Google shows users before sending them back. A client ID identifies your app to Google; the client secret is its password and stays on the server. 1. Create a Google Cloud project. 2. Configure the consent screen. 3. Create one Web OAuth client and copy its ID and secret. 4. Paste three values into `apps/web/.env.local` and restart the dev server. One project is enough for the whole product: the same Google Cloud project later hosts your Cloud Run deployment, and mobile sign-in flows through the same web backend — you do not create a separate client per platform or feature. [Ship the web app in 5 minutes](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Set up the database](https://supastack.dev/docs/web/setup-database.md) ## Create the Google Cloud project 1. Open [console.cloud.google.com](https://console.cloud.google.com) and sign in with your Google account. 2. Click the project dropdown in the top bar (next to the Google Cloud logo) and choose New Project. 3. Give it a name — your product name is fine — and click Create. No organization, card, or billing account is needed for sign-in credentials. Expected: the console reloads into the new project, and its name appears in the top bar. Reuse this project when you reach the deployment guide instead of creating another one. ## Configure the OAuth consent screen 1. In the left navigation open APIs & Services → OAuth consent screen and click Get started. 2. Enter the App name users will see (your product name), pick a User support email, choose External for the audience, fill in the developer contact email, and click through to Create. 3. Nothing else is needed on this screen: the app requests only the default `openid`, `email`, and `profile` scopes, which require no extra scopes or verification. Publishing status stays Testing while you develop. In that mode only listed accounts may sign in, so add your own Gmail address under Audience → Test users — otherwise Google blocks even you. When the product is live, click Publish app; because no sensitive scopes are requested, publishing does not start a Google verification review. ## Create the OAuth client 1. Open APIs & Services → Credentials → + Create Credentials → OAuth client ID. 2. Set Application type to Web application and name it something like `supastack local`. 3. Under Authorized JavaScript origins add `http://localhost:3000`. Under Authorized redirect URIs add `http://localhost:3000/api/auth/callback/google` — that callback path belongs to Better Auth, the kit's default auth engine: `{APP_URL}/api/auth/callback/google`. 4. Click Create and copy the Client ID and Client Secret from the dialog. You can reopen them on the client's page later, but copying now saves a step. For production create a second Web client (recommended) or add to this one: JavaScript origin `https://yourdomain.com` and redirect URI `https://yourdomain.com/api/auth/callback/google`. Keeping separate clients per environment means a leaked local secret cannot reach production. ## Add the values to the app Open `apps/web/.env.local` — Next.js reads this file, not the root `.env` — and set the three lines below. `AUTH_GOOGLE_ENABLED=true` turns the button on; the client pair identifies your Google Cloud client. The client secret is server-only: it must never appear in a `NEXT_PUBLIC_` or `EXPO_PUBLIC_` variable and never be committed to Git. In production, set the same three names in your host's secret store instead of an env file. Restart the dev server after editing — environment changes are only read at startup. ``` # apps/web/.env.local AUTH_GOOGLE_ENABLED=true GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret ``` ## Verify and troubleshoot Open http://localhost:3000/auth, click the Google button, complete Google's consent screen, and confirm the app comes back signed in. `redirect_uri_mismatch` means the redirect URI registered in Google does not match exactly — check the protocol (`http` vs `https`), the port, and the path: it must end `/api/auth/callback/google`, not `/callback/google/` or a trailing slash variant. Access blocked / has not completed the Google verification process means the consent screen is still in Testing and the signing-in account is not listed under Test users — add it under the Audience section. If the Google button is missing or sign-in still fails after editing `.env.local`, restart the dev server. If startup fails naming a variable, the flag is enabled without its complete client pair. [Add GitHub sign-in next](https://supastack.dev/docs/web/github-sign-in.md) [OAuth provider overview](https://supastack.dev/docs/web/setup-oauth.md) --- Source: [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) # Add GitHub sign-in Create a GitHub OAuth App per environment and wire Continue with GitHub into the app. ## Beginner walkthrough Prerequisites: the web app running locally and a migrated database for real accounts. You need a GitHub account — the same one that owns the repository is fine. Free, about 5 minutes per environment. Quick terms: a GitHub OAuth App is the registration that lets GitHub sign users in for you. Unlike most providers, one OAuth App allows exactly one Authorization callback URL — so local (`http://localhost:3000/…`) and production (`https://yourdomain.com/…`) each need their own app. Reusing one app and editing its callback back and forth will break one side every time. 1. Create a local OAuth App under your account's Developer settings. 2. Copy its Client ID and generate a Client Secret. 3. Set the three environment values and restart. 4. Repeat once with your real domain before going live. [Ship the web app in 5 minutes](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Set up the database](https://supastack.dev/docs/web/setup-database.md) ## Create the local OAuth App 1. Open [github.com](https://github.com) and sign in. Click your profile photo in the top-right corner → Settings. 2. Scroll to the bottom of the left sidebar and open Developer settings → OAuth Apps → New OAuth App. 3. Application name: something like `supastack local` — users see this name on GitHub's authorization page. Homepage URL: `http://localhost:3000`. Authorization callback URL: `http://localhost:3000/api/auth/callback/github`. Click Register application. 4. On the app's page copy the Client ID, click Generate a new client secret, and copy the secret immediately — GitHub shows it only once, though you can always generate another. ## Create the production OAuth App Repeat the same New OAuth App steps with your product's name, Homepage URL `https://yourdomain.com`, and Authorization callback URL `https://yourdomain.com/api/auth/callback/github`. Keep each app's credentials in its own environment: the local app's pair lives in `apps/web/.env.local`, the production app's pair lives in your host's secret store. The local secret must never reach production config and vice versa. ## Add the values to the app Set the three lines below in `apps/web/.env.local` using the local app's credentials. `AUTH_GITHUB_ENABLED=true` turns the button on; the pair identifies the OAuth App. The client secret stays server-only — never in `NEXT_PUBLIC_`/`EXPO_PUBLIC_` variables and never committed. Restart the dev server after editing. Do not reuse any `GITHUB_APP_*` values you may see elsewhere — those belong to an unrelated GitHub App integration, not to user sign-in. The kit's `docs/github-auth-setup.md` walks this whole flow end to end if you want a second reference. ``` # apps/web/.env.local — from the LOCAL OAuth App AUTH_GITHUB_ENABLED=true GITHUB_CLIENT_ID=your-oauth-app-client-id GITHUB_CLIENT_SECRET=your-oauth-app-secret ``` ## Verify and troubleshoot Open http://localhost:3000/auth and click the GitHub button. Authorize the app on github.com and confirm you return signed in. Until `AUTH_GITHUB_ENABLED=true` is set, the GitHub option on `/auth` remains visible but disabled — check the env file names, then restart. Setting the flag without both `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` fails startup and names the missing variable. A callback mismatch is the usual failure: the app's Authorization callback URL must equal `{APP_URL}/api/auth/callback/github` exactly — protocol, host, port, and path. A `redirect_uri` error after deploying usually means the request hit the local app's credentials while APP_URL says production, or the reverse. Restart the dev server after every env edit; values are read at startup only. [Set up the database next](https://supastack.dev/docs/web/setup-database.md) [OAuth provider overview](https://supastack.dev/docs/web/setup-oauth.md) --- Source: [Add Apple sign-in](https://supastack.dev/docs/web/apple-sign-in.md) # Add Apple sign-in Register an App ID, a Services ID, and a Sign in with Apple key in the Apple Developer portal, then wire the client pair into the app. ## Beginner walkthrough Prerequisites: the web app running locally (Ship in 5 minutes), a migrated database for real accounts (Set up database), and an Apple Developer Program membership — the same $99-per-year account the mobile accounts guide enrolls, so complete that step before continuing here if you have not already. Everything inside the developer portal below costs nothing extra. Why it matters: App Store review requires Sign in with Apple whenever an iOS app offers third-party social sign-in such as Google or GitHub, and it is a solid low-friction button on the web too — Apple users sign in with the account already on their devices. Quick terms: an App ID is your app's registered identifier (`com.yourcompany.yourapp`). A Services ID is a second identifier Apple issues for web and server sign-in — that one becomes your client ID. A return URL is the exact address Apple may send a signed-in user back to — the kit's Better Auth callback. A secret key is the `.p8` file Apple gives you once; you turn it into the client secret yourself. 1. Register an App ID with the Sign in with Apple capability. 2. Register a Services ID that points at it and holds your domain plus return URL. 3. Create a Sign in with Apple key and download the `.p8` file. 4. Turn the key into a client-secret JWT and set the three env values. One honest limitation: Apple's return URLs must be absolute `https://` addresses on a registered domain — `localhost` and plain `http` are rejected, so the button cannot be fully exercised on a bare local server. Verify on your deployed URL, or expose the local server through an https tunnel (for example `cloudflared tunnel` or `ngrok`) and register the tunnel domain plus its callback while testing. [Create the Apple Developer account](https://supastack.dev/docs/mobile/external-accounts.md) [Set up the database](https://supastack.dev/docs/web/setup-database.md) ## Register the App ID 1. Open [developer.apple.com/account](https://developer.apple.com/account) and sign in with the account holding the Developer Program membership. 2. Open Certificates, Identifiers & Profiles → Identifiers and click the blue + button. 3. Select App IDs → Continue, keep the type set to App → Continue. 4. Description: your product name. Bundle ID: choose Explicit and enter the reverse-domain form `com.yourcompany.yourapp` — if you ship the iOS app, use exactly the `ios.bundleIdentifier` in `apps/mobile/app.json` so both platforms share one App ID. 5. In the capability list tick Sign in with Apple → Continue → Register. Expected: the identifier appears in the Identifiers list with the Sign in with Apple capability enabled. Reuse this one App ID — the Services ID and the key both attach to it as the primary app. ## Register the Services ID and return URL 1. Back in Identifiers click + again, choose Services IDs → Continue. 2. Description: something like `yourapp web sign-in`. Identifier: another reverse-domain name — `com.yourcompany.yourapp.web` is the convention — deliberately different from the App ID. This Services ID is the client ID the server uses for web sign-in; copy it exactly, then Continue → Register. 3. Select the new Services ID in the list, tick Sign in with Apple, and click Configure. 4. In the dialog set Primary App ID to the App ID from the previous section. Under Website URLs add your domain (for example `yourdomain.com`), and under Return URLs add `https://yourdomain.com/api/auth/callback/apple` — that path is the kit's Better Auth callback, `{APP_URL}/api/auth/callback/apple`. Save → Continue → Save. Each return URL must be an absolute `https://` address on a registered domain: `localhost`, bare IP addresses, and `http` are rejected. For local development run the app behind an https tunnel, add the tunnel domain to the Services ID, register `https:///api/auth/callback/apple`, and set `APP_URL` to the tunnel URL while testing. Expected: the Services ID reads Sign in with Apple → Enabled. If the portal rejects your domain, finish domain ownership verification on the account page first. ## Create the Sign in with Apple key 1. Open Keys in the left sidebar → +. Name the key (for example `yourapp sign-in key`), tick Sign in with Apple, click Configure, choose the App ID as the Primary App ID → Save → Continue → Register. 2. On the confirmation screen record the Key ID, then click Download — the `.p8` file is offered exactly once. If you lose it, revoke the key and create another; Apple does not keep a copy. 3. Note your Team ID too — it is printed in the top-right corner of the developer account pages, next to your account name. Expected: you now hold three values — the `.p8` file, a Key ID, and your Team ID. Keep the `.p8` file out of Git: it signs as your developer account. ## Add the values to the app The kit expects exactly the three lines below — the same `clientId`/`clientSecret` pair every social provider uses. `APPLE_CLIENT_ID` is the Services ID (`com.yourcompany.yourapp.web`), not the App ID. Apple issues no static client secret, so `APPLE_CLIENT_SECRET` is a JWT you sign yourself from the Team ID, the Key ID, and the `.p8` private key — the Better Auth documentation ships the short generator script for it. A signed secret stays valid at most six months: set a reminder to regenerate it and update the env value, because sign-in fails the moment it lapses. Never paste the `.p8` file's contents or the raw Key ID into env — only the generated JWT belongs there. The client secret and the `.p8` file are server-only: never in `NEXT_PUBLIC_`/`EXPO_PUBLIC_` variables and never committed — in production set them through your host's secret store. Restart the dev server after editing; env changes are read at startup. ``` # apps/web/.env.local AUTH_APPLE_ENABLED=true APPLE_CLIENT_ID=com.yourcompany.yourapp.web # the Services ID, not the App ID APPLE_CLIENT_SECRET= ``` ## Verify and troubleshoot Open your `APP_URL` — a deployed URL or the https tunnel — at `/auth`, click the Apple button, sign in with any Apple ID (it does not need to be the developer account), and confirm you return signed in. Most failures are one of three mix-ups: Services ID vs App ID — the client ID must be the Services ID for web sign-in; a return-URL mismatch — the callback must equal a registered Return URL exactly, protocol, domain and path included; or an expired/invalid client secret — regenerate the JWT, which is only a signed token, not the `.p8` contents. Until `AUTH_APPLE_ENABLED=true` is set, the Apple option on `/auth` stays disabled — check the env names and restart. Enabling the flag without both `APPLE_CLIENT_ID` and `APPLE_CLIENT_SECRET` fails startup and names the missing variable. The iOS app reaches this same backend — ship Sign in with Apple inside it so App Store review accepts the other social buttons. The mobile accounts guide explains what review looks for. [Create the mobile accounts](https://supastack.dev/docs/mobile/external-accounts.md) [Add Stripe payments next](https://supastack.dev/docs/web/stripe-payments.md) [OAuth provider overview](https://supastack.dev/docs/web/setup-oauth.md) --- Source: [Add Stripe payments](https://supastack.dev/docs/web/stripe-payments.md) # Add Stripe payments Create a free Stripe account, a product with a recurring price, a secret key, and the signed webhook that grants entitlements after checkout. ## Beginner walkthrough Prerequisites: the app running locally (Ship in 5 minutes) and a migrated database for real accounts (Set up database). You need an email address for the Stripe account — no card and no charges. Everything in this guide happens in test mode: fake money, test card numbers, unlimited retries before going live. Quick terms: test mode is Stripe's sandbox — the dashboard's Test mode toggle keeps every key, product, and payment fake until you switch it off. A secret key (`sk_test_…`) is your server's password to Stripe. A product with a price is what customers buy; a price id (`price_…`) names it in code. A webhook is Stripe calling your server when a payment succeeds or a subscription changes, and the signing secret (`whsec_…`) proves the call really comes from Stripe. What you create: a Stripe account, one product with a recurring price, a secret API key, and a webhook endpoint — about twenty minutes. Stripe hosts the checkout page itself; the kit creates the session server-side and redirects, so no publishable browser key is needed. Keep scope in mind: this guide sells web subscriptions only. Purchases inside the iOS/Android app must go through Apple and Google billing — never Stripe Checkout — which is why mobile uses RevenueCat instead. [Set up the database](https://supastack.dev/docs/web/setup-database.md) [Set up RevenueCat for mobile purchases](https://supastack.dev/docs/mobile/revenuecat.md) ## Create the Stripe account 1. Open [stripe.com](https://stripe.com) → Start now and sign up with your email. 2. The onboarding asks about your business — for test mode you can skip every question and still use the full sandbox; complete activation only before taking real payments. Expected: you land on the dashboard with the Test mode toggle visible in the header. Keep it on for everything below — live keys belong in your production secret store later, never in a laptop env file. ## Copy the API keys 1. In the dashboard open Developers → API keys. 2. You see two test keys: a Publishable key `pk_test_…` and a masked Secret key. Click Reveal test key next to the secret and copy the `sk_test_…` value. The kit only needs the secret key: checkout sessions are created on the server and the user is redirected to Stripe's hosted page, so no publishable key lands in env vars. If a product you build later adds Stripe.js you would use the publishable key client-side; the kit's billing flow does not. The `sk_test_…` key is a full credential for the sandbox account: it lives in `apps/web/.env.local` or a secret store, never in `NEXT_PUBLIC_`/`EXPO_PUBLIC_` variables and never in Git. ## Create the product and price 1. Open Product catalog → Add product. 2. Name it after your plan — `Pro` matches the kit's shipped plan — give it a price, and choose Recurring (monthly, for example): the kit's checkout creates subscription-mode sessions, which require a recurring price, not a one-time one. 3. Save, open the product, and copy the price id — it starts `price_…` (not `prod_…`, which is the product id). The shipped `pro` plan already shows a Monthly/Yearly toggle: monthly checks out with `STRIPE_PRICE_ID`, and the yearly option needs a second recurring price in `STRIPE_PRICE_ID_PRO_YEARLY` — without it, a yearly checkout fails `price-unconfigured`. Change plan names, amounts, and what a purchase grants in `apps/web/src/product-plans.ts` — Stripe stores the money side, the plan file stores the entitlement side. ## Add the webhook endpoint The webhook is what actually grants access: the kit flips the entitlement only after a signed Stripe event arrives, so a checkout redirect alone proves nothing. 1. Open Developers → Webhooks → Add endpoint. 2. Endpoint URL: your public URL plus `/api/billing/webhook` — for example `https://yourdomain.com/api/billing/webhook`. The other billing providers get their own path under that directory; Stripe's receiver lives at the root. 3. Under Select events choose the events the kit handles: `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `checkout.session.async_payment_failed`, `invoice.paid`, `invoice.payment_failed`, `customer.subscription.created`, `customer.subscription.updated`, and `customer.subscription.deleted`. 4. Create the endpoint, open its page, click Reveal under Signing secret, and copy the `whsec_…` value — each endpoint has its own secret. Local development cannot receive Stripe's servers, so use the Stripe CLI instead: `stripe login` once, then `stripe listen --forward-to localhost:3000/api/billing/webhook` in a second terminal. It prints a temporary `whsec_…` — that value, not the dashboard one, goes in `.env.local` while `stripe listen` runs. ## Add the values to the app Set the lines below in `apps/web/.env.local`. `BILLING_PROVIDER=stripe` selects the adapter; the other three are the key, the price, and the signing secret. Enabling the provider with any of them missing fails startup and names the variable. All three `STRIPE_*` values are server-only — never `NEXT_PUBLIC_`/`EXPO_PUBLIC_`, never committed; production gets them through the host's secret manager (the deploy guide covers Secret Manager). Restart the dev server after editing. ``` # apps/web/.env.local BILLING_PROVIDER=stripe STRIPE_SECRET_KEY=sk_test_… STRIPE_PRICE_ID=price_… STRIPE_WEBHOOK_SECRET=whsec_… # `stripe listen` value locally; endpoint signing secret in production # STRIPE_PRICE_ID_PRO_YEARLY=price_… # required for the shipped Yearly toggle ``` ## Verify and troubleshoot Start `stripe listen --forward-to localhost:3000/api/billing/webhook` and leave it running, restart the dev server, then open `/billing`, choose the Pro plan, and finish Stripe's hosted checkout with the test card `4242 4242 4242 4242`, any future expiry, and any CVC. Expected: the app returns to `/billing?checkout=success` and the workspace's entitlement flips active — the listen terminal logs the delivered events, and Developers → Webhooks → your endpoint on the dashboard shows green deliveries. To exercise the receiver without clicking through checkout, `stripe trigger checkout.session.completed` sends a synthetic event through the listen forwarder. Common failures: a 400 on every delivery means the signing secret does not match that endpoint — recopy `whsec_…` for the right endpoint and update env; silent no-ops mean `BILLING_PROVIDER` is still `local`; and `sk_live_` keys mixed into a test setup mean you copied the wrong-mode keys — keep test and live strictly separate. The kit ships the same one-contract adapter for Lemon Squeezy, Creem, Polar, Whop, Paddle, Dodo, and PayPal — same flow, different console and env names — and mobile purchases stay on RevenueCat, not Stripe. [Compare the billing providers](https://supastack.dev/docs/web/setup-payments.md) [Add Resend email next](https://supastack.dev/docs/web/resend-email.md) [Mobile purchases use RevenueCat](https://supastack.dev/docs/mobile/revenuecat.md) --- Source: [Add Resend email](https://supastack.dev/docs/web/resend-email.md) # Add Resend email Create a free Resend account, verify a sending domain, and switch EMAIL_PROVIDER so codes and receipts reach real inboxes. ## Beginner walkthrough Prerequisites: the app running locally (Ship in 5 minutes). Start free — no card, and Resend's free tier covers development volumes. Quick terms: `EMAIL_PROVIDER=local` is the default — messages are written to the built-in `/mail-preview` page and never leave your machine, which is perfect while building. Resend is the adapter that delivers to real inboxes. `EMAIL_FROM` is the sender identity templates use; its address must sit on a domain Resend has verified. DNS records are the lines you paste at your registrar or Cloudflare so receiving mailboxes trust your sender. What you create: a Resend account, a verified sending domain, and one API key — the domain step is the only waiting part, since DNS propagation runs from minutes to hours. [Set up the database](https://supastack.dev/docs/web/setup-database.md) ## Create the Resend account 1. Open [resend.com](https://resend.com) → Sign up, confirm your email, and you land on the dashboard. 2. Start with the free tier — you can even send test mail from Resend's shared sender before owning a domain, which the next section covers. ## Verify your sending domain Quick try, no domain: the address `onboarding@resend.dev` works as `EMAIL_FROM` immediately — but it only delivers to the email you signed up with, never to users. Use it to see the pipeline work, then do the real setup. 1. Open Domains → Add Domain and enter the domain you send from (for example `yourdomain.com`; a subdomain such as `mail.yourdomain.com` also works and keeps records tidy). 2. Resend lists the DNS records to create — DKIM TXT records plus MX/SPF or CNAME entries depending on account age. Add each one at your DNS host: your registrar's DNS page, or Cloudflare if you moved nameservers. On Cloudflare keep the proxy cloud grey (DNS only) for mail records — proxied records never verify. 3. Back in Resend click Verify. Expected: the domain flips to Verified. Propagation varies from minutes to a day; the domain page names the exact record that still fails to resolve. Once verified, any address on the domain works as a sender — `hello@`, `no-reply@`, whatever `EMAIL_FROM` uses — no per-address registration needed. ## Create the API key 1. Open API Keys → Create API Key. 2. Permission: Sending access is enough — the kit only sends. Name the key for its environment (`production`, `local`) so a leaked dev key can be revoked without touching production. 3. Copy the `re_…` key now — it is shown once. Losing it just means creating another. ## Add the values to the app Set the lines below in `apps/web/.env.local`. `RESEND_API_KEY` is server-only — never `NEXT_PUBLIC_`/`EXPO_PUBLIC_`, never committed; `EMAIL_PROVIDER` and `EMAIL_FROM` are plain configuration. Restart the dev server after editing. `EMAIL_FROM` must sit on the verified domain: `onboarding@resend.dev` only reaches your own signup address, and an unverified domain gets a 403 rejection from Resend. ``` # apps/web/.env.local EMAIL_PROVIDER=resend EMAIL_FROM="Your Product " RESEND_API_KEY=re_… ``` ## Verify and troubleshoot Restart, open `/auth`, and request a sign-in code to a real inbox you control. Expected: the message arrives, and Resend → Emails shows the delivery with a green status — that log is also where bounce and error reasons live. Nothing sent? Check `EMAIL_PROVIDER=resend` actually saved in `apps/web/.env.local` and restart. A 403 in the server log means `EMAIL_FROM` is not on the verified domain — or `onboarding@resend.dev` was sent to someone other than yourself. Different provider later? `EMAIL_PROVIDER` also accepts `ses` (with the AWS keys) and the `postmark`, `sendgrid`, `mailgun`, `smtp`, `brevo` transports — same `EMAIL_FROM` contract, different env names, all listed in `.env.example`. The email guide compares them. [Compare the email providers](https://supastack.dev/docs/web/setup-email.md) [Collect the remaining provider keys next](https://supastack.dev/docs/web/provider-keys.md) --- Source: [Set up Amazon SES email](https://supastack.dev/docs/web/aws-ses.md) # Set up Amazon SES email Set up Amazon SES email for web, mobile and desktop with AWS SDK credentials or IAM SMTP, verify your sender, and avoid unnecessary Mail Manager endpoint fees. ## 1. Verify a sender in your AWS region Open Amazon SES in US East (N. Virginia), us-east-1. Choose Configuration → Identities → Create identity, select your domain or email address, and finish the DNS or email verification. Use that same region and verified address in the settings below. New SES accounts start in a sandbox: verify the recipient too, or use the SES mailbox simulator. Request production access from Account dashboard before sending to ordinary customer addresses. [Open Amazon SES](https://us-east-1.console.aws.amazon.com/ses/home?region=us-east-1) ## 2. For SMTP, choose IAM SMTP credentials In SES, open SMTP settings → Create SMTP credentials. Choose IAM SMTP credentials instead of the Recommended Mail Manager option. The IAM card says: Manual credential management. No traffic policies or rule sets. Click Create IAM credentials, name the sending user, and download the SMTP credentials before closing the page. [Create IAM credentials](https://us-east-1.console.aws.amazon.com/iam/home?SESRegion=us-east-1#/users/smtp/create) · [Manage existing](https://us-east-1.console.aws.amazon.com/iam/home#/users) These links use us-east-1, where classic IAM SMTP is available. For another supported region, create credentials for that region and use its SMTP endpoint. The downloaded SMTP password is different from an AWS secret access key; use it only with the SMTP settings in step 5. [AWS SMTP credential instructions](https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html) ## 3. Avoid unnecessary Mail Manager charges IAM SMTP and the SES SDK send directly through SES without Mail Manager traffic policies or rule sets. Avoid creating an optional open or mTLS Mail Manager ingress endpoint: AWS lists it at $50 per month per endpoint under à la carte pricing. Current authenticated endpoints are complimentary, so the Recommended option does not always add $50. Normal SES sending charges and your chosen pricing plan still apply. [Check current AWS pricing](https://aws.amazon.com/ses/pricing/) ## 4. Send through the AWS SDK Choose this mode to send through the SES API over HTTPS. In IAM, give the backend role or a dedicated sending user ses:SendEmail permission on your verified SES identity. For a user, open Security credentials → Create access key and save the access key ID and original secret access key. An SMTP password cannot sign SDK requests. Put the example below in apps/web/.env.local for development or your backend host’s secret settings for production. Replace the sender and placeholder values. With an IAM role or configured local AWS profile, leave both access-key fields and AWS_SESSION_TOKEN blank; the SDK uses its default credential chain. If you provide explicit keys, provide both. For temporary explicit credentials, also set AWS_SESSION_TOKEN to the matching session token and refresh all three values when they expire. Role-based credentials refresh through the SDK. [AWS SDK credential chain](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) ``` EMAIL_PROVIDER=ses EMAIL_FROM=Your App AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= # Temporary explicit credentials only: AWS_SESSION_TOKEN= ``` ## 5. Or send with classic IAM SMTP Use the SMTP username and SMTP password from step 2 with this alternative configuration. Port 587 uses STARTTLS; SMTP_SECURE=false selects that connection mode. Use the endpoint for the same region as the credentials and verified sender. These are the buyer kit’s settings, shared by all three platforms. Choose one email mode at a time; EMAIL_PROVIDER=smtp uses SMTP credentials, while EMAIL_PROVIDER=ses uses SDK API credentials. ``` EMAIL_PROVIDER=smtp EMAIL_FROM=Your App SMTP_HOST=email-smtp.us-east-1.amazonaws.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER= SMTP_PASSWORD= ``` ## 6. Configure once for web, mobile and desktop Restart the backend after changing settings. With a migrated database, request one email sign-in code at /auth using a permitted recipient, then check the inbox and spam folder. The mobile sign-in flow calls the same backend; desktop loads the web app. Neither client needs AWS keys or an SMTP password. Keep credentials on the backend only: never put them in NEXT_PUBLIC_ or EXPO_PUBLIC_ settings or the desktop bundle. If delivery fails, check sender verification, sandbox recipient restrictions, region, IAM permission and credential type. A queued response means SES accepted the message, not that it reached the inbox. [Email setup overview](https://supastack.dev/docs/web/setup-email.md) --- Source: [Collect the remaining provider keys](https://supastack.dev/docs/web/provider-keys.md) # Collect the remaining provider keys Short walkthroughs for the services left: AI providers, Trigger.dev, S3 storage, Sentry or PostHog, Turnstile, and your domain. ## How to use this page Each section below is one complete setup walkthrough: make the account, find the key, set the env names, verify. Take only the sections for services your product ships — a site with no AI features never needs the first one, and the kit keeps running with every section here skipped. Same secret rules as everywhere: API keys and webhook secrets stay server-only, only values prefixed `NEXT_PUBLIC_`/`EXPO_PUBLIC_` may reach the client, production secrets live in the host's secret store, and the dev server rereads env only at restart. [Back to the accounts checklist](https://supastack.dev/docs/web/external-accounts.md) ## AI provider keys The kit's `AI_PROVIDER` accepts `openai`, `anthropic`, `openai-compatible`, and `openrouter` — plus `fake`, the default local stand-in. Pick one; the provider key is the only account needed, because model ids and prices come from the kit's database catalog, not env vars. OpenAI: [platform.openai.com](https://platform.openai.com) → API keys → Create new secret key → copy `sk-…` → `OPENAI_API_KEY`. Anthropic: [console.anthropic.com](https://console.anthropic.com) → API Keys → Create Key → `sk-ant-…` → `ANTHROPIC_API_KEY`. Both bill per token — a few dollars of credit covers development. OpenRouter routes every catalog model through one key: [openrouter.ai](https://openrouter.ai) → Keys → Create Key → `sk-or-…` → `AI_PROVIDER=openrouter` plus `OPENROUTER_API_KEY`; optional `OPENROUTER_APP_NAME`/`OPENROUTER_SITE_URL` send attribution headers. `openai-compatible` covers any OpenAI-shaped endpoint: `OPENAI_COMPATIBLE_API_KEY` plus `OPENAI_COMPATIBLE_BASE_URL`. Verify: restart, run one AI task, and expect a real model answer instead of the deterministic fake. A startup error naming the key means the provider flag is on without its key. A real reply also needs `JOB_PROVIDER=trigger` from the Trigger.dev section below plus one enabled model in the kit's catalog — [Set up AI](https://supastack.dev/docs/web/setup-ai.md) covers that setup. ``` # apps/web/.env.local — pick ONE provider AI_PROVIDER=openai OPENAI_API_KEY=sk-… # or AI_PROVIDER=anthropic # ANTHROPIC_API_KEY=sk-ant-… # or AI_PROVIDER=openrouter # OPENROUTER_API_KEY=sk-or-… # or AI_PROVIDER=openai-compatible # OPENAI_COMPATIBLE_API_KEY=… # OPENAI_COMPATIBLE_BASE_URL=https://your-endpoint/v1 ``` ## Trigger.dev Skip unless the product needs real background-job runs — `JOB_PROVIDER=local` queues jobs in memory without executing them, which is right for development. 1. Open [app.trigger.dev](https://app.trigger.dev) → sign up → create a project. 2. Copy two values: the project ref `proj_…` from the project's settings or dashboard URL → `TRIGGER_PROJECT_REF`, and the secret key from the project's API keys page (`tr_dev_…` for the dev environment, `tr_prod_…` for production) → `TRIGGER_SECRET_KEY`. Both stay server-only. 3. The kit's tasks already live in `apps/web/trigger` and `apps/web/trigger.config.ts` reads `TRIGGER_PROJECT_REF`. From `apps/web` run `npx trigger.dev@latest dev` — it registers the tasks with your project and runs them locally against the dev key. Verify: the dev command logs the registered tasks; queue one in the app and watch it execute in the dashboard's Runs list. Deployed environments run `corepack pnpm --filter @supastack/web trigger:deploy` to publish the task set. ``` # apps/web/.env.local JOB_PROVIDER=trigger TRIGGER_PROJECT_REF=proj_… TRIGGER_SECRET_KEY=tr_dev_… # tr_prod_… in production # run the worker alongside the app, from apps/web: cd apps/web npx trigger.dev@latest dev ``` ## S3-compatible storage (Cloudflare R2) Skip unless the product stores files — `STORAGE_PROVIDER=memory` keeps uploads in-process for development. Any S3-compatible bucket works; Cloudflare R2 is the free-tier-friendly pick. 1. Open [dash.cloudflare.com](https://dash.cloudflare.com) → R2 Object Storage → Create bucket, name it, and keep the default location hint. 2. Back on the R2 overview open Manage R2 API Tokens → Create API token → permission Object Read & Write, scoped to that bucket → Create. Copy the Access Key ID, the Secret Access Key, and the S3 endpoint shown — `https://.r2.cloudflarestorage.com`. 3. Set the env names below. `STORAGE_REGION=auto` is the R2 convention — the kit defaults `us-east-1`, which R2 also accepts. `STORAGE_FORCE_PATH_STYLE` already defaults on for custom endpoints, which R2 wants. Serving public files additionally needs `STORAGE_PUBLIC_BASE_URL` or CDN wiring; private files need nothing more. Verify: restart and upload through the app — the object appears in the R2 bucket browser. A signature or 403 error means the token lacks object read/write on that bucket; create a fresh token scoped correctly. ``` # apps/web/.env.local STORAGE_PROVIDER=s3 STORAGE_ENDPOINT=https://.r2.cloudflarestorage.com STORAGE_REGION=auto STORAGE_BUCKET=your-bucket STORAGE_ACCESS_KEY_ID=… # server-only STORAGE_SECRET_ACCESS_KEY=… # server-only # STORAGE_PUBLIC_BASE_URL= # optional public-URL base ``` ## Sentry or PostHog (optional) Entirely optional: every observability value is unset by default and the kit runs fine with none — add this when you want error reports and analytics in production. Sentry for errors: [sentry.io](https://sentry.io) → create a project → pick the Next.js platform → the setup screen shows the DSN `https://…@….ingest.sentry.io/…` → `SENTRY_DSN`. Exceptions then reach Sentry tagged with correlation ids; the free tier is enough to start. PostHog for product analytics instead: [posthog.com](https://posthog.com) → create a project → copy the project API key `phc_…` → `NEXT_PUBLIC_POSTHOG_KEY` plus `NEXT_PUBLIC_POSTHOG_HOST` (`https://us.i.posthog.com`, or your EU host). Those two are public browser values by design; `SENTRY_DSN` stays server-side. `ALERT_WEBHOOK_URL` optionally posts failure alerts to a Slack-compatible endpoint. Verify: restart, raise a test exception (or replay Sentry's sample event), and confirm it lands in the project's Issues list. ``` # apps/web/.env.local — all optional SENTRY_DSN=https://…@….ingest.sentry.io/… NEXT_PUBLIC_POSTHOG_KEY=phc_… NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # ALERT_WEBHOOK_URL=https://hooks.slack.com/services/… ``` ## Cloudflare Turnstile Bot protection for the public forms and credential endpoints — `CAPTCHA_PROVIDER=off` by default, so add it before opening sign-up to strangers. Free. 1. Open [dash.cloudflare.com](https://dash.cloudflare.com) → Turnstile → Add widget → name it and list the hostnames: `localhost` for development plus your real domain → Create. 2. Copy both values: the Site Key — public by design, it renders inside the widget → `NEXT_PUBLIC_TURNSTILE_SITE_KEY` — and the Secret Key, which the server uses to verify → `TURNSTILE_SECRET_KEY`. 3. `CAPTCHA_PROVIDER=turnstile` switches it on; startup fails naming whichever key is missing. The captcha guide lists exactly which surfaces the widget protects. Verify: open `/auth` or the contact form — the widget renders; submissions without a solved challenge are rejected. If the widget never loads, the hostname you are browsing is not on the widget's list. [See which forms Turnstile protects](https://supastack.dev/docs/web/captcha.md) ``` # apps/web/.env.local CAPTCHA_PROVIDER=turnstile NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x… # public by design TURNSTILE_SECRET_KEY=0x… # server-only ``` ## A domain of your own One purchase at any registrar — Cloudflare Registrar, Porkbun, Namecheap all work. Nothing here copies into env; the domain shows up in the Services ID, Resend DNS, Turnstile hostnames, and OAuth return URLs you configured along the way. Two ways to point it at the app: for Cloud Run use the domain mapping in the deploy guide — a few console clicks plus the DNS records Google lists. Or move DNS to Cloudflare nameservers (the registrar swaps two records) — optional, but it is also the tidy way to manage R2 public buckets and Turnstile hostnames in one place. [Map the domain on Cloud Run](https://supastack.dev/docs/web/deploy-google-cloud.md) [Set up the database next](https://supastack.dev/docs/web/setup-database.md) --- Source: [Deploy to Google Cloud](https://supastack.dev/docs/web/deploy-google-cloud.md) # Deploy to Google Cloud Build the kit's Docker image on Cloud Build, run it on Cloud Run with managed secrets, and put it on your domain. ## Beginner walkthrough Prerequisites: the app working locally, a real database you can reach from the internet (Set up database), and the provider accounts for the features you ship. You need a Google account; billing setup asks for a card even though light usage stays inside the free tier. Quick terms: a container image is a packaged copy of your app that any host can run. Artifact Registry is Google's store for images. Cloud Build builds the image on Google's machines — no local Docker needed. Cloud Run runs the image as an HTTPS service and scales it. Secret Manager holds passwords and keys. A revision is one deployed version of the service. What you are building: a Google Cloud project, an Artifact Registry repository holding your image, a Cloud Run service serving the app over HTTPS, secrets in Secret Manager, and a managed Postgres. The kit's root `Dockerfile` already produces a standalone Next.js server listening on port 8080 — exactly what Cloud Run expects. Honest cost note: Cloud Run's free tier covers light traffic, but Cloud SQL — Google's managed Postgres — has no free tier. A Supabase or Neon free-tier database is the cheapest way to start; this guide uses that path. Consider adding a budget alert under Billing → Budgets & alerts before experimenting. [Set up the database](https://supastack.dev/docs/web/setup-database.md) [Create the OAuth accounts first](https://supastack.dev/docs/web/external-accounts.md) ## Create the account and install the CLI 1. Open [console.cloud.google.com](https://console.cloud.google.com), sign in with your Google account, and accept the terms. 2. Attach billing when prompted: Billing → Link a billing account → Create billing account. A card is required to activate the account; trial credit covers early experiments. 3. Click the project dropdown → New Project, name it, and Create — or reuse the project you made for Google sign-in; one project is fine. 4. Install the Google Cloud CLI from [cloud.google.com/sdk](https://cloud.google.com/sdk/docs/install): download the installer for your operating system, complete it, then open a new terminal so the `gcloud` command is available. 5. Run the two commands below. The first opens a browser to authorize the CLI; the second selects your project — use the project ID shown on the console dashboard, which is not always identical to the name. Expected: `gcloud config list` shows your account and project. If `gcloud` is not recognized, reopen the terminal or finish the installer first. ``` gcloud auth login gcloud config set project YOUR_PROJECT_ID ``` ## Create the production database Beginner path — managed Postgres outside Google. At [supabase.com](https://supabase.com): New project → choose a name, database password, and region → wait for provisioning → Project Settings → Database. Supabase offers two connection strings: the session pooler / direct connection on port 5432, and the transaction pooler on port 6543 — the 5432 string is for migrations and long-lived clients, the 6543 string is for the serverless app. At [neon.tech](https://neon.tech): New project → copy the connection string it shows. Either works; keep the password portion intact. Put the direct/session string into `DATABASE_URL` in the root `.env` — the migration tool reads the root file and needs a session connection — and in `apps/web/.env.local` for the local dev server. Then run the migrate command once from the repository root to create the kit's tables in the new database. The `DATABASE_URL` secret the deployed app uses may be the pooled 6543 string instead. The Google-native alternative is Cloud SQL, a managed Postgres inside the same project. It has no free tier and private-IP connections require a VPC connector — treat it as a later upgrade, not a starting point. ``` # DATABASE_URL= in .env + apps/web/.env.local, then: corepack pnpm --filter @supastack/db db:migrate ``` ## Enable services and push the image Run the commands below from the repository root. They enable the four Google services this deployment uses, create a Docker repository in Artifact Registry, and submit the repository to Cloud Build — which builds the root `Dockerfile` remotely, so Docker Desktop is not required. Replace `YOUR_PROJECT_ID`, and pick one region — `us-central1` below — and reuse it for the repository and the service. Expected: the build log streams for several minutes and ends with `DONE` / `STATUS: SUCCESS`. The image then appears in the console under Artifact Registry → supastack → web. ``` gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com secretmanager.googleapis.com gcloud artifacts repositories create supastack --repository-format=docker --location=us-central1 gcloud builds submit --tag us-central1-docker.pkg.dev/YOUR_PROJECT_ID/supastack/web:latest ``` ## Store secrets in Secret Manager Secrets must never go into `--set-env-vars` or the image — both are stored in plain text and visible to anyone who can read the service configuration. Secret Manager keeps them encrypted and injects them at runtime. 1. In the console open Security → Secret Manager → Create Secret. 2. Create `DATABASE_URL` with the full Postgres connection string — the pooled serverless string is fine for the app — `AUTH_SECRET` with at least 32 random characters (`openssl rand -hex 32` generates one), and `CREDENTIAL_ENCRYPTION_KEY` with a Base64-encoded 32-byte key (`openssl rand -base64 32`); it encrypts the provider keys workspaces store in the app. 3. Create one secret per provider you enabled — for example `STRIPE_SECRET_KEY` plus `STRIPE_WEBHOOK_SECRET`, `RESEND_API_KEY`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_SECRET`. Non-secret settings such as `EMAIL_PROVIDER=resend` stay plain environment variables, not secrets. The first time a service references a secret, its runtime service account needs permission to read it — when you attach secrets through the console's service editor, Google offers that grant automatically; on the CLI the deploy error names the missing Secret Manager role to grant. ``` # Secrets to create in Secret Manager: DATABASE_URL # Postgres connection string — the pooled serverless string is fine AUTH_SECRET # openssl rand -hex 32 CREDENTIAL_ENCRYPTION_KEY # openssl rand -base64 32 # plus one per enabled provider, e.g.: STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET RESEND_API_KEY GOOGLE_CLIENT_SECRET GITHUB_CLIENT_SECRET ``` ## Deploy the Cloud Run service Console path: Cloud Run → Create service → Deploy one revision from an existing container image → select the pushed `…/supastack/web:latest` image → choose the same region → under Authentication choose Allow unauthenticated invocations (it is a public app) → Create. The CLI equivalent below adds the environment wiring in the same step. Plain-text configuration goes in `--set-env-vars`; secrets are referenced by their Secret Manager name in `--set-secrets`. Add your provider flags (`EMAIL_PROVIDER`, `BILLING_PROVIDER`, `AI_PROVIDER`, `JOB_PROVIDER`) as plain env vars alongside `APP_URL`. `APP_ENV=production` is required — without it the app boots in `local` mode: production config validation is skipped, secure cookie prefixes stay off, and the dev-only `/mail-preview` page remains publicly reachable (leaking the local email outbox when `EMAIL_PROVIDER=local`). Production then requires four more values at boot: `EXPO_PUBLIC_API_URL` is the public https URL the mobile app uses to reach the API — set it to the service URL (same as `APP_URL` for a web-only deploy); `EXPO_PUBLIC_APP_SCHEME` is the mobile deep-link scheme — any valid value such as `supastack` works when you ship no mobile app; `MOBILE_ALLOWED_ORIGINS` is the comma-separated list of exact https origins the server trusts — your app URL is the right start, and wildcards are rejected; `CREDENTIAL_ENCRYPTION_KEY` encrypts stored provider credentials and stays in Secret Manager. Cloud Run assigns a `https://….run.app` URL on the first deploy, and the app's `APP_URL` must equal its real public URL for auth callbacks and links to work. So deploy once with the placeholder, copy the `*.run.app` URL from the service page, then update the service (console: Edit & deploy new revision, or rerun the command) with `APP_URL` — and the matching `EXPO_PUBLIC_API_URL` / `MOBILE_ALLOWED_ORIGINS` values — set to it. The container port stays 8080 — the Cloud Run default already matches the `Dockerfile`'s `PORT`. ``` gcloud run deploy supastack --image us-central1-docker.pkg.dev/YOUR_PROJECT_ID/supastack/web:latest --region us-central1 --allow-unauthenticated --set-env-vars "APP_ENV=production,APP_URL=https://YOUR_SERVICE_URL,EXPO_PUBLIC_API_URL=https://YOUR_SERVICE_URL,EXPO_PUBLIC_APP_SCHEME=supastack,MOBILE_ALLOWED_ORIGINS=https://YOUR_SERVICE_URL" --set-secrets "DATABASE_URL=DATABASE_URL:latest,AUTH_SECRET=AUTH_SECRET:latest,CREDENTIAL_ENCRYPTION_KEY=CREDENTIAL_ENCRYPTION_KEY:latest" ``` ## Map your own domain (optional) Skip this while testing — the `*.run.app` URL already serves HTTPS. When you are ready: Cloud Run → your service → Custom Domains / Domain Mappings → Add mapping → pick the service and enter your domain. Verify domain ownership if prompted (Google walks you through it), then create the DNS records the mapping screen lists at your registrar. The managed HTTPS certificate is issued automatically after the records propagate — usually minutes, up to a day. After the domain serves traffic, update `APP_URL` to `https://yourdomain.com` and register the matching OAuth redirect URIs (`https://yourdomain.com/api/auth/callback/…`) — the sign-in guides cover where. ## Verify the deployment Open `https://YOUR_SERVICE_URL/api/health` — expect `ok`. Then exercise the app end to end: open the URL, sign up, request an email code, confirm it reaches the inbox (that needs `EMAIL_PROVIDER=resend`, `RESEND_API_KEY`, and a verified sender — the local adapter never reaches real inboxes), sign in, and use one feature you enabled. Cloud Run → your service → Logs shows request and error output. The Revisions tab offers one-click rollback to the previous revision if a deploy goes wrong. Mobile and desktop clients must point at this HTTPS backend: `EXPO_PUBLIC_API_URL` in `apps/mobile/.env.local` for Expo, `SUPASTACK_APP_URL` for the desktop shell. [Create the mobile accounts](https://supastack.dev/docs/mobile/external-accounts.md) [Run the full deployment checklist](https://supastack.dev/docs/web/deployment.md) --- Source: [Set up database](https://supastack.dev/docs/web/setup-database.md) # Set up database Local Postgres or a managed Supabase project — then run the migrations. ## What a migration is A fresh database is empty — it has no tables for users, workspaces or records. A migration is a saved, ordered database change: a file that says create this table, add that column. The kit ships its migrations ready-made; the `db:migrate` command applies every pending one to the database named by `DATABASE_URL`. Running it is not optional setup — it is what turns an empty Postgres into your app's database. Sign-in, workspaces, billing records and saved data all live in those tables. That is why accounts break if sign-in happens before migrations: the app writes the new user into a table that does not exist yet, leaving a half-created account. Always migrate first — the setup steps below do exactly that before the first real sign-in. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Install and start Docker Desktop. 2. Copy .env.example to .env in the root for migration tools and to apps/web/.env.local for Next.js (PowerShell: Copy-Item .env.example .env; Copy-Item .env.example apps/web/.env.local. macOS/Linux/Git Bash: cp .env.example .env; cp .env.example apps/web/.env.local). Keep DATABASE_URL the same in both files; the example already matches the local Docker database. 3. From the repository root run docker compose up -d postgres and docker compose ps; wait until postgres reports healthy. 4. Run corepack pnpm --filter @supastack/db db:migrate from the root to apply the migrations already shipped with the kit. Only after editing packages/db/src/schema.ts, run db:generate, inspect the generated SQL, then run db:migrate. Expected: the local Postgres container is healthy and the Web app uses its migrated database. Never point this first run at production. Configure DATABASE_URL for the intended Postgres database in the server environment. Use a separate disposable database for integration acceptance. Never aim test migrations at a production database. The database workspace exports schema definitions and persistence helpers. Review packages/db/src/schema.ts and the migration directory before introducing a table or column. ``` docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate # Only after changing packages/db/src/schema.ts: corepack pnpm --filter @supastack/db db:generate corepack pnpm --filter @supastack/db db:migrate # Optional Compose / Supabase settings in .env: # POSTGRES_PASSWORD=supastack # SUPABASE_ACCESS_TOKEN= # DATABASE_POOLER=transaction ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Choose one copy method for your terminal: # PowerShell: Copy-Item .env.example .env Copy-Item .env.example apps/web/.env.local # macOS/Linux/Git Bash alternative: # cp .env.example .env # cp .env.example apps/web/.env.local docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate ``` ## Implementation workflow Generate a migration after editing the schema, inspect the generated SQL, and commit it with the schema change. Plan how existing records receive values before adding a required column. Include workspaceId on every user-owned record and carry that scope into reads and writes. Use immutable model-price history for AI accounting instead of rewriting prices used by existing usage records. ## Verification and troubleshooting If Docker says it cannot connect to its engine, start Docker Desktop and wait for it to finish loading. If postgres is not healthy, run docker compose logs postgres from the root and fix that error before migrating. If Web says DATABASE_URL is missing, check apps/web/.env.local; if db:migrate cannot connect, check the root .env. If you change databases, update both files. Read the first failed migration statement before retrying; never repeat it against production. Apply migrations to an isolated database and run database-gated tests there. Missing credentials should produce an explicit skipped gate, not an assertion that persistence passed. If migration fails, inspect which statement failed and the current schema before retrying. --- Source: [Set up payments](https://supastack.dev/docs/web/setup-payments.md) # Set up payments Pick a billing adapter and wire its signed webhook to entitlements. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local and keep BILLING_PROVIDER=local while learning the checkout screen. 2. For a real integration, create test-mode products in one provider, then add its server keys and webhook secret to the Web host's private environment settings. A webhook is a signed message the billing service sends to your app after checkout, renewal, or cancellation; the endpoint path is listed in this guide's code example. 3. Start Web with corepack pnpm --filter @supastack/web dev and test only the sandbox. Expected: a verified webhook updates the signed-in workspace's entitlement; opening a checkout page alone does not grant access. Use sandbox/test credentials while integrating. Configure BILLING_PROVIDER plus the provider's server-only secrets: stripe → STRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET; lemonsqueezy → LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_VARIANT_ID, LEMONSQUEEZY_WEBHOOK_SECRET; creem → CREEM_API_KEY, CREEM_PRODUCT_ID, CREEM_WEBHOOK_SECRET (CREEM_TEST_MODE=true for the sandbox); polar → POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, POLAR_WEBHOOK_SECRET (POLAR_SERVER); whop → WHOP_API_KEY, WHOP_PLAN_ID, WHOP_WEBHOOK_SECRET; paddle → PADDLE_API_KEY, PADDLE_PRICE_ID, PADDLE_WEBHOOK_SECRET (PADDLE_ENVIRONMENT); dodo → DODO_PAYMENTS_API_KEY, DODO_PRODUCT_ID, DODO_PAYMENTS_WEBHOOK_KEY (DODO_PAYMENTS_ENVIRONMENT); paypal → PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_PLAN_ID, PAYPAL_WEBHOOK_ID (PAYPAL_ENVIRONMENT). The price/product must represent the intended recurring subscription. ``` Webhook endpoint: /api/billing/webhook (stripe), /api/billing/webhook/lemonsqueezy, /api/billing/webhook/creem, /api/billing/webhook/polar, /api/billing/webhook/whop, /api/billing/webhook/paddle, /api/billing/webhook/dodo, /api/billing/webhook/paypal Checkout endpoint: /api/billing/checkout Portal endpoint: /api/billing/portal ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Follow the lifecycle event list in docs/real-provider-smoke.md when configuring the webhook endpoint. Signature verification requires the endpoint's matching webhook secret. A checkout redirect alone must never grant access. The entitlement at the server action boundary decides whether paid work is permitted. Process repeated events safely and scope customer/subscription records to the correct workspace. Web checkout remains separate from native purchases. The key a plan grants is product vocabulary shared across surfaces: ProductPlan.entitlement in src/product-plans.ts declares the lowercase machine key written into the ledger while a plan is paid (the kit ships 'ai.reference-task' on pro and lifetime; src/billing-runtime.ts passes it to the BFF). The same key is what a CMS entry's required_entitlement field and the EntitlementGate component's entitlementKey prop equality-compare against — an entry gated on 'ai.reference-task' unlocks for any workspace holding that grant, while a key no plan grants stays locked for everyone. ## Verification and troubleshooting If a webhook is rejected, compare the exact sandbox URL and signing secret in the provider dashboard with the server settings, and check whether that provider requires forwarding requests to localhost. If checkout works but access stays locked, inspect the webhook result and the entitlement key on the plan. Never test with live payment credentials while following this local guide. Test activation, updates, cancellation, invalid signatures, and repeated events in the sandbox. Inspect persisted access state after processing. Creating an unconsumed checkout session proves connectivity only, not lifecycle fulfillment. --- Source: [Set up email](https://supastack.dev/docs/web/setup-email.md) # Set up email Transactional delivery: pick a provider, verify a sender, preview every template. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local and set EMAIL_PROVIDER=local for development; EMAIL_FROM is the sender label used by templates. 2. Start the Web app from the repository root with corepack pnpm --filter @supastack/web dev and trigger one sign-in or invite flow with a test address. Expected: the app records a local email outcome without contacting a real provider. Only connect Resend, SES, or another service after its sender domain is verified and test recipients are restricted. EMAIL_PROVIDER=local keeps deterministic delivery local. For configured production delivery, review the Resend or SES adapter and provide its server credentials. EMAIL_FROM must match the identity verified with the selected provider. [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/web/aws-ses.md) ``` EMAIL_PROVIDER=local EMAIL_FROM=supastack # Production adapter configuration is listed in .env.example: # RESEND_API_KEY= POSTMARK_SERVER_TOKEN= SENDGRID_API_KEY= BREVO_API_KEY= # MAILGUN_API_KEY= MAILGUN_DOMAIN= MAILGUN_REGION=us ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Authentication and completion messages use the transactional email boundary. Outreach workflows belong to the consuming product and remain separate. Never reuse this path as an unreviewed bulk messaging subsystem. Record delivery separately from generation. A model result can be available while its completion message fails. Recovery must inspect delivery state instead of generating the same result again. ## Verification and troubleshooting If no local email appears, check that EMAIL_PROVIDER is set in apps/web/.env.local and restart Web. If a real provider rejects a message, confirm EMAIL_FROM matches a verified sender and inspect the provider's delivery record. A generated AI answer can succeed even when its follow-up email fails; check delivery before running the AI task again. Verify recipient restrictions, sender identity, and provider errors in a dedicated test environment. A local adapter result is not evidence of real email delivery. The opt-in smoke path restricts recipients through REAL_SMOKE_EMAIL_ALLOWED_DOMAIN. --- Source: [Set up OAuth sign-in](https://supastack.dev/docs/web/setup-oauth.md) # Set up OAuth sign-in Google, Apple, GitHub, Microsoft, and Twitter — one flag plus a client pair each. ## Create and verify a real account Authentication means proving who is using the app. Start Postgres and run migrations using the database guide first, then leave the Web server running. Open http://localhost:3000/auth, choose email/password sign-up, enter your name, email and password, and submit. Keep EMAIL_PROVIDER=local for this first test. When a flow requests an email code, open http://localhost:3000/mail-preview in another browser tab, find the most recent message for that address and copy its code back into the sign-in form. Request a fresh code if it expires. These messages stay in the local outbox; repeatedly checking an external inbox will not find them. Sign out, then sign in again to confirm the database-backed account works. If sign-up reports a database connection or missing-table error, finish database setup instead of enabling social login. Google, GitHub, Apple, X and Microsoft buttons need their own provider application and callback settings; enable one only after its provider guide is complete. Better Auth is the shipped default engine and does not need a paid authentication account for local email sign-in. [Start Postgres before creating an account](https://supastack.dev/docs/web/database.md) [Use and troubleshoot email sign-in codes](https://supastack.dev/docs/web/setup-ai.md) [Configure Google sign-in](https://supastack.dev/docs/web/setup-oauth.md) [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/web/apple-sign-in.md) ## Choose enabled sign-in methods The foundation includes email/password, OTP, and configurable social authentication. Better Auth owns identity and sessions, while the application composes provider configuration and transactional OTP delivery. Enable only the social providers you have configured. Provider client secrets and the authentication secret stay on the server. ## Connect identity to a workspace Authenticated handlers resolve session state before accessing workspace records. Workspace provisioning must remain idempotent so replaying a lifecycle hook does not create duplicate ownership records. ## Verify session behavior Test sign-in, invalid or expired OTPs, single-use consumption, sign-out, and rejected anonymous access. Memory-adapter tests cover handler semantics; production database persistence and real provider callbacks need separate acceptance. ## Setup and configuration 1. Set AUTH_SECRET to at least 32 random characters — openssl rand -hex 32 works. 2. In each provider's developer console, create an OAuth app and register the callback URL: {APP_URL}/api/auth/callback/ under Better Auth (the default AUTH_PROVIDER). Use separate apps for local and production where the provider allows only one callback — docs/github-auth-setup.md walks the GitHub flow end to end. 3. Flip the provider's flag and set its client pair — AUTH_GOOGLE_ENABLED plus GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, and the matching trio for Apple, GitHub, Microsoft, or Twitter. Enabling a flag without its credentials fails startup and names the missing variable. 4. Microsoft is the one callback that differs by engine: /api/auth/callback/microsoft under Better Auth, /api/auth/callback/microsoft-entra-id under Auth.js. ``` AUTH_SECRET= AUTH_PASSKEY_ENABLED=true EARLY_ACCESS_INVITE_ONLY=false AUTH_GOOGLE_ENABLED=true GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… AUTH_GITHUB_ENABLED=true GITHUB_CLIENT_ID=… GITHUB_CLIENT_SECRET=… # AUTH_APPLE_ENABLED / AUTH_MICROSOFT_ENABLED / AUTH_TWITTER_ENABLED # follow the same pattern: # MICROSOFT_CLIENT_ID=… MICROSOFT_CLIENT_SECRET=… # TWITTER_CLIENT_ID=… TWITTER_CLIENT_SECRET=… # Workspace connected accounts (Settings → Connections): # CONNECTED_X_CLIENT_ID= CONNECTED_X_CLIENT_SECRET= # CONNECTED_GOOGLE_CLIENT_ID= CONNECTED_GOOGLE_CLIENT_SECRET= # CONNECTED_MICROSOFT_CLIENT_ID= CONNECTED_MICROSOFT_CLIENT_SECRET= # Callbacks to register (Better Auth): # {APP_URL}/api/auth/callback/google # {APP_URL}/api/auth/callback/apple # {APP_URL}/api/auth/callback/github # {APP_URL}/api/auth/callback/twitter # {APP_URL}/api/auth/callback/microsoft (Auth.js: microsoft-entra-id) ``` ## Implementation workflow Workspace external accounts are a different surface: the CONNECTED_X_*, CONNECTED_GOOGLE_*, and CONNECTED_MICROSOFT_* pairs feed Settings → Connections, and their callbacks live under {APP_URL}/api/accounts/{x|google|microsoft}/callback. An empty client id leaves that provider's connect flow disabled — don't reuse sign-in app credentials there. Provider client secrets stay server-only, exactly like AUTH_SECRET — never in NEXT_PUBLIC_ or EXPO_PUBLIC_ variables. Sign-in working? Continue to Set up AI. ## Verification and troubleshooting Test first-time sign-in, a returning session, a cancelled consent screen, and a disabled provider. A callback mismatch is the usual failure — the registered URL must match APP_URL and the engine's callback id exactly, and the app needs a restart after env changes. --- Source: [Set up AI](https://supastack.dev/docs/web/setup-ai.md) # Set up AI Real models behind cheap / fast / smart aliases — keys stay on the server. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local for the local Web server and apps/web/src/agent-runtime.ts for provider selection; .env.example is the list of supported setting names. 2. Keep AI_PROVIDER=fake for local interface work unless you have your own provider account and want to test real generation; never put provider keys in a NEXT_PUBLIC_ or EXPO_PUBLIC_ setting. 3. Start the Web app from the repository root with corepack pnpm --filter @supastack/web dev. Expected: local screens can be explored without configuring a paid provider; actual generation requires a valid server provider and enabled model choices in the database catalog. The default AI_PROVIDER=fake supports local deterministic work. Real provider configuration belongs in the server environment. The provider keys listed in .env.example include OpenAI, Anthropic, and an OpenAI-compatible endpoint. AI_MODEL_POLICY contains ordered model IDs for cheap, fast, and smart aliases. An empty alias list disables that choice. Model IDs must correspond to enabled database catalogue entries with effective immutable pricing. ``` AI_MODEL_POLICY='{"cheap":[],"fast":["catalog-model-id"],"smart":["catalog-model-id"]}' AGENT_FRAMEWORK=ai-sdk # ai-sdk | mastra | langgraph | openai-agents ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Configure the catalogue and workspace policy before accepting production tasks. The browser receives enabled aliases, while capability matching, model selection, entitlement checks, and budget enforcement stay on the server. Keep credentials out of NEXT_PUBLIC_ and EXPO_PUBLIC_ variables. For bring-your-own-key mode, use the existing encrypted envelope and server decryption boundary; never return a stored provider key to the interface. ## Verification and troubleshooting If the app reports that no AI model is available, check the server's AI_PROVIDER, the enabled models and prices in the admin catalog, and the cheap/fast/smart alias policy. If a real provider returns an authentication error, check its key in apps/web/.env.local and restart the server. A key prefixed NEXT_PUBLIC_ would be visible to browsers and must be removed. Verify disabled aliases, missing capabilities, inactive entitlement, and exhausted budgets. Use a separately authorized provider smoke run for actual generation. If no alias is available, inspect server policy and catalogue configuration rather than exposing a raw model selector. --- Source: [Set up background jobs](https://supastack.dev/docs/web/setup-jobs.md) # Set up background jobs Durable tasks through Trigger.dev — plus the local scheduler tick. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/src/job-handlers.ts and apps/web/src/scheduler-runtime.ts in VS Code. A job is background work; the scheduler decides when to enqueue it. 2. For a local tick, set the same SCHEDULER_TICK_SECRET in apps/web/.env.local and the repository-root .env; the tick script reads the root .env, while the Web endpoint reads its app settings. 3. Start Web with corepack pnpm --filter @supastack/web dev, then run corepack pnpm jobs:tick from the root. Expected: the terminal shows a POST result from localhost:3000. Trigger.dev is a separate provider and needs its own account and server keys. Set JOB_PROVIDER=trigger with server-only TRIGGER_SECRET_KEY and TRIGGER_PROJECT_REF for production task submission. Review apps/web/trigger.config.ts and the ai.generate task in apps/web/trigger before configuring the target project. ``` JOB_PROVIDER=trigger TRIGGER_PROJECT_REF=your-project-reference # Provide TRIGGER_SECRET_KEY through the server secret store. # Local/self-hosted scheduler tick: # SCHEDULER_TICK_SECRET=… # SCHEDULER_TICK_URL=http://localhost:3000 ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Terminal 1 — start the Web app from the monorepo root: corepack pnpm --filter @supastack/web dev # Terminal 2 — run one scheduler tick from the monorepo root: corepack pnpm jobs:tick ``` ## Implementation workflow The shared JobRunner contract promises enqueue, status, and cancel. Trigger.dev owns workflow-specific semantics; do not infer scheduling, retries, or durability guarantees from the generic interface. The durable application task ID connects dispatch, worker execution, usage records, and results. A failed provider-run metadata attachment after confirmed enqueue must not cause another enqueue or invalidate accepted work. ## Verification and troubleshooting If the tick returns 404, confirm the secret exists in both settings files and matches exactly, then restart Web. If the terminal says connection refused, start Web first. If no job ran, check that a schedule is enabled and due; one tick does not make a future schedule run immediately. Verify the deployed task identifier and project match the configured environment. Separate enqueue acceptance from workflow completion and from email delivery. Read docs/production-ai-runtime.md for recovery and safe diagnostics before manually retrying work. --- Source: [Set up file storage](https://supastack.dev/docs/web/setup-storage.md) # Set up file storage Workspace-scoped uploads on memory or any S3-compatible bucket. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. For a no-credential trial, keep `STORAGE_PROVIDER=memory`. If this checkout has no visible upload control, ask your coding agent to inspect `apps/web/app/api/storage/avatar/route.ts`, `apps/web/app/api/storage/logo/route.ts`, `apps/web/app/api/storage/organization-logo/route.ts`, and the matching settings page before wiring a small sample image. 2. To check persistence, set `STORAGE_PROVIDER=s3`, `STORAGE_REGION`, and `STORAGE_BUCKET` in `apps/web/.env.local`, add server-only test credentials locally, and restart. 3. The image should reload from storage. Memory files disappear when the process restarts; for S3 errors, check bucket, endpoint, and access policy. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Select the storage backend `STORAGE_PROVIDER=memory` is the credential-free default. `local` writes objects to disk under `STORAGE_LOCAL_DIR` (default `./.data/storage`, required explicitly in production) and streams them through HMAC-signed expiring URLs at `/api/storage/local/[...key]` (`STORAGE_LOCAL_SIGNING_SECRET`, falling back to `AUTH_SECRET`) for single-server deployments. `s3` uses `apps/web/src/storage-runtime.ts` to compose the AWS SDK client over the narrow `@supastack/storage` contract, supporting Supabase Storage and MinIO with path-style URLs enabled by default. ``` STORAGE_PROVIDER=s3 STORAGE_REGION=us-east-1 STORAGE_BUCKET=your-product-bucket # Set STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY on the server. # Optional: STORAGE_ENDPOINT, STORAGE_FORCE_PATH_STYLE, STORAGE_PUBLIC_BASE_URL # Self-hosted single-server disk storage: # STORAGE_PROVIDER=local # STORAGE_LOCAL_DIR=./.data/storage # STORAGE_LOCAL_SIGNING_SECRET= # Local MinIO Compose overrides: MINIO_ROOT_USER / MINIO_ROOT_PASSWORD ``` ## Reuse the upload boundaries The shipped routes cover objects, avatars, profile logos and organization logos under `/api/storage/`. They resolve identity/workspace permissions and validate upload shape before storage. Inspect the chosen route's size/type policy rather than accepting arbitrary client object keys. Public URLs are presentation, not authorization. Keep protected content behind the application's verified read boundary and configure bucket/CDN policy for your product's data. ## Test real storage behavior Test allowed/denied uploads, MIME and size rejection, foreign-workspace paths, missing objects and provider failure. Memory mode does not persist across processes; verify storage and retrieval against the selected backend before release. [Supabase Storage setup](https://supastack.dev/docs/web/project-structure.md) [Upload route inventory](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration 1. STORAGE_PROVIDER=memory (the default) keeps uploads in-process — right for development and tests. 2. For real files, set STORAGE_PROVIDER=s3 with a bucket and an access key pair. STORAGE_ENDPOINT selects any S3-compatible service — Supabase Storage at https://.supabase.co/storage/v1/s3, MinIO, or AWS itself. 3. STORAGE_FORCE_PATH_STYLE defaults to true once an endpoint is set — Supabase and MinIO require it. Set it false only for a virtual-hosted custom endpoint. 4. Optionally point STORAGE_PUBLIC_BASE_URL at a CDN so public object URLs resolve there. ``` STORAGE_PROVIDER=s3 STORAGE_ENDPOINT=https://.supabase.co/storage/v1/s3 STORAGE_REGION=us-east-1 # Supabase: project region STORAGE_BUCKET= STORAGE_ACCESS_KEY_ID=… STORAGE_SECRET_ACCESS_KEY=… STORAGE_PUBLIC_BASE_URL=https://cdn.example.com # optional # No bucket yet? Local S3-compatible storage ships in compose: docker compose --profile storage up -d minio # console on :9001 ``` ## Implementation workflow Uploads and reads go through authenticated routes that resolve the workspace first — /api/storage/objects plus the dedicated avatar and logo routes. Every stored key is namespaced as {workspaceId}/{purpose}/{safeName} via buildWorkspaceObjectKey. Credentials stay server-only; the browser never sees the bucket or keys. Uploads working? Continue to Set up observability. ## Verification and troubleshooting Verify an upload round-trips, a second workspace cannot read the object, and oversized or disallowed content types fail. Signature or 403 errors against Supabase or MinIO usually mean STORAGE_FORCE_PATH_STYLE is wrong for the endpoint. --- Source: [Set up observability](https://supastack.dev/docs/web/setup-observability.md) # Set up observability Error tracking, product analytics, and failure alerts — all optional. ## Find useful information when an action fails Observability means seeing what your app did. Begin with the terminal running your Web server: repeat one failing action and read the first error and any correlation ID, a label linking messages from the same request. Share the command, time and sanitized error when asking for help; do not share .env files or tokens. For hosted error reports, follow Health and operations to configure the supported monitoring settings in apps/web/.env.local or the host's environment dashboard. Save and restart the server. Trigger a controlled test failure in a test environment and confirm it appears with the correct workspace context. Monitoring being configured does not prove it received an event. ALERT_WEBHOOK_URL is an optional HTTPS destination for job and terminal webhook-delivery failure alerts. If you need it, create a compatible incoming endpoint and save its URL server-side; otherwise leave it empty. Ask an agent to simulate one failure and confirm exactly one alert, with no secrets in its text. Do not trigger paid work or failures in customer accounts for this test. [Check application health and operational settings](https://supastack.dev/docs/web/security.md) [Diagnose an error in the local terminal](https://supastack.dev/docs/web/troubleshooting.md) ## Keep a workspace audit trail Audit records capture changes such as provider configuration updates within a workspace. Resolve the authenticated workspace before reading or writing these records, and preserve that scope when adding product-specific events. Record the action and identifiers needed to understand it. Provider credentials and other sensitive values must stay out of audit payloads and logs. ## Capture product events The observability package exposes an AnalyticsClient adapter and workspace-scoped AnalyticsEvent values. Each event carries a name, distinct identity, workspaceId, and optional properties. Use createObservability to route events through your configured analytics client. Structured properties are redacted before delivery; keep sensitive free-form text out of event names and property values as well. ## Report errors with safe context ErrorClient captures exceptions with workspaceId tags and structured extra context. The shared observability layer sanitizes exceptions and redacts sensitive context before forwarding them to the configured client. Use local adapters to verify captured events and exceptions without calling a monitoring provider. Test that workspace tags remain present and credentials are removed before enabling an external destination. ## Follow a request across tasks, jobs and webhooks Entry boundaries mint a corr_ correlation id that links the originating request to the work it triggers. AI-task submission stamps the id on the task payload and the ai_task_submitted event, every billing webhook receiver mints one per request, and the scheduler mints one per dispatched job. The same id rides job.completed and ai_task_completed event properties, captureException extras, the webhook_event ledger payload and each webhook_delivery payload meta — so it lands inside the signed body a subscriber receives. Missing or malformed ids are tolerated: the consumer mints a fresh id instead of rejecting older rows. ## Alert on real failures Set ALERT_WEBHOOK_URL to any HTTPS endpoint that accepts a JSON POST shaped {text, name, severity, context}; a Slack-style incoming webhook works as-is. Alerts are fire-and-forget with a five-second timeout, a failed delivery logs one console warning, and the alerter noops while the variable is unset. The shipped alerts are job.failed (warning — a job handler threw or a job type is unknown) and webhook.delivery_failed (critical — only when an outgoing delivery reaches terminal failure after its final attempt; ordinary retries stay quiet and each delivery row alerts at most once). ## Measure durations and stream structured logs serverObservability.measure times an async operation and emits a .duration analytics event carrying durationMs — on success and on failure, without masking the original error. The ai.task.execute job run is already wrapped, so task execution duration lands in the configured analytics client. serverObservability.logger writes one JSON line per record to stdout with environment and service base fields; the shared redaction layer strips secrets before any line is emitted. ## Setup and configuration 1. Set SENTRY_DSN for error reporting — exceptions are captured with correlation IDs and workspace tags. 2. Set NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST for product analytics — these are public browser values by design. 3. Set ALERT_WEBHOOK_URL to a Slack-compatible endpoint for failure alerts — the app POSTs {text, name, severity, context} JSON. ``` SENTRY_DSN=https://…@….ingest.sentry.io/… NEXT_PUBLIC_POSTHOG_KEY=phc_… NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com ANALYTICS_PROVIDER=off # off | local | plausible | posthog # ANALYTICS_SITE_ID= # ANALYTICS_HOST= ALERT_WEBHOOK_URL=https://hooks.slack.com/services/… ``` ## Implementation workflow The observability layer sanitizes exceptions and redacts known secret keys before forwarding — but free-form event text is the author's responsibility, so keep credentials out of event names and property values. Local adapters let tests assert on captured events and exceptions without calling Sentry or PostHog; none of these variables are required to run locally. Signals flowing? Continue to Set up custom domains. ## Verification and troubleshooting Trigger a test exception and confirm it lands in Sentry with secrets stripped from the payload, then check that a wired failure POSTs to ALERT_WEBHOOK_URL. If events never arrive, verify the DSN/key pair and that the host is reachable from the server. --- Source: [Set up custom domains](https://supastack.dev/docs/web/setup-domains.md) # Set up custom domains Let workspaces serve the app on their own hostnames via Cloudflare for SaaS. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local and use CUSTOM_DOMAINS_PROVIDER=local to preview the Domains screen without DNS changes. 2. For a real customer hostname, configure Cloudflare for SaaS with your own zone, an API token limited to that zone, and the CNAME target; enter the server-only values on the Web host, not in the browser. 3. Run Web with corepack pnpm --filter @supastack/web dev and open Settings → Domains as a workspace owner/admin. Expected: the local adapter activates a test domain immediately; the Cloudflare adapter waits for the displayed DNS records. Set CUSTOM_DOMAINS_PROVIDER=cloudflare with CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, and CUSTOM_DOMAIN_CNAME_TARGET. The API token needs SSL and Certificates:Edit permission on the zone (the Custom Hostnames API lives under /zones/{zone_id}/custom_hostnames). The CNAME target is a hostname in your Cloudflare zone — for example customers.example.com — that resolves to your deployed app. Add a DNS record in the zone pointing the CNAME target at your origin (the fallback origin in Cloudflare for SaaS terms). Cloudflare issues each custom hostname a DCV TXT record plus an optional ownership TXT record; the settings screen shows customers exactly which records to create. For local development use CUSTOM_DOMAINS_PROVIDER=local, which marks every hostname active immediately with target customers.localhost. CUSTOM_DOMAINS_PROVIDER=off (the default) hides the section's controls. ``` CUSTOM_DOMAINS_PROVIDER=cloudflare CLOUDFLARE_API_TOKEN=cf_token_with_ssl_edit CLOUDFLARE_ZONE_ID=zone_id CUSTOM_DOMAIN_CNAME_TARGET=customers.example.com ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Workspace owners and admins manage domains under Settings → Domains (five per workspace). Customers point a CNAME record for their hostname at the CNAME target, add the TXT records shown for ownership and certificate validation, then press Refresh status. Once Cloudflare reports the hostname active it becomes a trusted origin for auth on that deployment and the workspace name/logo brand the app chrome on that hostname. Apex (root) domains cannot be CNAMEs at most DNS providers — customers should use a subdomain such as portal.example.com, or a provider with CNAME flattening. Cookies stay scoped to each host, so a customer domain is a separate sign-in; single sign-on across custom domains is out of scope. ## Verification and troubleshooting If Cloudflare stays pending, compare the customer's CNAME and TXT records with the exact values shown on the domain card and allow DNS time to update. Use a subdomain when the DNS provider cannot flatten a root-domain CNAME. If the menu is absent, check that the feature is enabled and the signed-in user is a workspace owner or admin. Pending for a long time usually means the customer's CNAME is missing or still propagating — re-check the DNS records shown on the domain card. A moved or failed status means Cloudflare can no longer resolve the hostname to the zone; remove and re-add it. Validation data is stored as provider JSON so future Cloudflare validation shapes need no schema change. --- Source: [Authentication](https://supastack.dev/docs/web/authentication.md) # Authentication Sign-in methods, sessions, and account security. ## Create and verify a real account Authentication means proving who is using the app. Start Postgres and run migrations using the database guide first, then leave the Web server running. Open http://localhost:3000/auth, choose email/password sign-up, enter your name, email and password, and submit. Keep EMAIL_PROVIDER=local for this first test. When a flow requests an email code, open http://localhost:3000/mail-preview in another browser tab, find the most recent message for that address and copy its code back into the sign-in form. Request a fresh code if it expires. These messages stay in the local outbox; repeatedly checking an external inbox will not find them. Sign out, then sign in again to confirm the database-backed account works. If sign-up reports a database connection or missing-table error, finish database setup instead of enabling social login. Google, GitHub, Apple, X and Microsoft buttons need their own provider application and callback settings; enable one only after its provider guide is complete. Better Auth is the shipped default engine and does not need a paid authentication account for local email sign-in. [Start Postgres before creating an account](https://supastack.dev/docs/web/database.md) [Use and troubleshoot email sign-in codes](https://supastack.dev/docs/web/setup-ai.md) [Configure Google sign-in](https://supastack.dev/docs/web/setup-oauth.md) [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/web/apple-sign-in.md) ## Choose enabled sign-in methods The foundation includes email/password, OTP, and configurable social authentication. Better Auth owns identity and sessions, while the application composes provider configuration and transactional OTP delivery. Enable only the social providers you have configured. Provider client secrets and the authentication secret stay on the server. ## Connect identity to a workspace Authenticated handlers resolve session state before accessing workspace records. Workspace provisioning must remain idempotent so replaying a lifecycle hook does not create duplicate ownership records. ## Verify session behavior Test sign-in, invalid or expired OTPs, single-use consumption, sign-out, and rejected anonymous access. Memory-adapter tests cover handler semantics; production database persistence and real provider callbacks need separate acceptance. ## Setup and configuration Better Auth is the default engine (AUTH_PROVIDER=better-auth); an Auth.js engine is available for teams standardizing on it. Enabled methods are declared per provider: email/password, one-time codes delivered by transactional mail, and social sign-in for Google, Apple, GitHub, Microsoft, and Twitter. Each social provider switches on with its AUTH__ENABLED flag plus the matching client id and secret. The auth secret, provider secrets, and session signing keys are server-only configuration. ``` AUTH_PROVIDER=better-auth AUTH_SECRET=generate-a-long-random-value AUTH_GOOGLE_ENABLED=true GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… ``` ## Implementation workflow The /auth surface renders the configured methods; the server composes the engine in auth-runtime and resolves the session at every protected boundary. A successful first sign-in provisions the member's personal workspace idempotently — replayed lifecycle hooks must never create a second ownership record. Account security lives under /settings/security: password change, active sessions, and passkeys where the build enables them. Keep session restore, sign-out revocation, and cross-workspace denial as server-enforced behavior, not interface state. ## Verification and troubleshooting Exercise sign-in, invalid and expired OTP codes, single-use consumption, passkey ceremonies on HTTPS origins, social callback returns, sign-out, and anonymous denial. Register every provider redirect URI before testing callbacks, and verify that a revoked session cannot keep performing protected work. --- Source: [Organizations and invites](https://supastack.dev/docs/web/organizations.md) # Organizations and invites Ship the demo-style org loop: create, switch, invite, roles, leave, and danger-zone delete. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Start the Web app from the repository root with corepack pnpm --filter @supastack/web dev and create a test account. 2. Open the organization setup screen, create a test workspace, then invite a second test email from Settings → Members. A workspace is the team boundary that owns members and records. 3. Accept the invitation while signed in with that same email. Expected: the second account can enter the invited workspace with its assigned role, but cannot manage owner-only actions. Organization workspaces are distinct from personal workspaces in packages/db. After signup, onboarding gates (/onboarding, /new-organization, /select-organization, /choose-plan) push the user into an active org before product billing and settings. Invites create a pending workspace_invitation row and send transactional email with a /invite/[token] accept URL. Accept requires a matching signed-in email. Member list, role updates, and leave live under /settings/members; rename/delete and leave also appear on /organization/settings. ``` Routes /new-organization /select-organization /invite/[token] /settings/members /organization/settings Server actions createOrganizationAction createWorkspaceInviteAction acceptWorkspaceInviteAction updateMemberRoleAction leaveOrganizationAction renameOrganizationAction deleteOrganizationAction ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Resolve the active workspace from cookie → user preference → membership, then verify membership on every write. Owners and admins manage invites/roles; sole owners cannot leave or demote themselves. Prefer evaluateLeaveOrganization and evaluateMemberRoleUpdate from @supastack/db before mutating memberships. Emit member.left / member.role_changed in-app notifications after successful mutations when inAppProduct is enabled. Platform administration is a separate shipped boundary: only platform admins can manage users, bans and impersonation. A workspace owner/admin role does not grant platform access. The documented organization roles are owner, admin and member. ## Verification and troubleshooting If an invite link says the email does not match, sign in using the exact address that received it. If the invitation email is missing, check the local email result before resending. If a member cannot see an organization, switch to the correct workspace and verify membership; never fix this by passing another workspaceId from the browser. Focused Vitest: workspace leave/role helpers, invite acceptance, organization-onboarding gates. Manual: create org → invite → accept on a second account → update role → leave as non-owner → delete as owner with typed confirm. --- Source: [Onboarding flow](https://supastack.dev/docs/web/onboarding.md) # Onboarding flow The gated path from sign-up to an active workspace and plan. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration After sign-up, onboarding gates push the user through workspace setup before product work: /onboarding, /new-organization, /select-organization, and /choose-plan. The organization-onboarding module decides which step applies from the member's current state. The flow is resumable — every gate re-derives the correct step server-side, so a user who leaves mid-way lands on the right screen when they return. ``` /onboarding entry gate /new-organization create a team workspace /select-organization pick an existing one /choose-plan plan selection before billing /invite/[token] invited members join here ``` ## Implementation workflow Treat each step as a gate, not a wizard page — the server evaluates state and redirects to the next required step. Invited members skip creation and land on accept-invite instead. When adding a product gate — a required profile field, a terms acknowledgement — extend the onboarding evaluation rather than scattering redirects through layouts. ## Verification and troubleshooting Test a fresh account through every step, a returning user mid-flow, an invited member, and a member of multiple organizations. Verify no step renders the app shell before its gate passes. --- Source: [Payments and subscriptions](https://supastack.dev/docs/web/billing.md) # Payments and subscriptions Stripe checkout, plan management, and the entitlement boundary. ## Choose and test your payment service Billing handles payments. Access permissions are stored separately as entitlements. Complete persisted account setup, then open /billing in the running Web app to see the plan interface. With BILLING_PROVIDER=local, you are exploring local behavior rather than collecting real money. Choose one supported payment provider, follow its setup guide, create your own test product and price, and copy the named server settings into apps/web/.env.local. Save and restart the Web server. Complete one checkout using that provider's sandbox instructions, then inspect the workspace's access state after the provider's webhook arrives. A webhook is the provider's signed message to your server about a payment event. A success page alone does not unlock features. If checkout completes but access remains inactive, check the provider's webhook delivery and your server's event processing before repeating payment. Test cancellation and expired access as well. Phone purchases use the separate RevenueCat/store setup, not this Web checkout. [Choose a billing adapter and its settings](https://supastack.dev/docs/modules/billing-configuration.md) [Set your own products and prices](https://supastack.dev/docs/web/project-structure.md) [Set up Stripe in a test environment](https://supastack.dev/docs/web/setup-payments.md) ## Web billing The Web application supports the billing provider you select with BILLING_PROVIDER; Stripe is one option. Open /billing for Web plans and subscription management. Follow the selected provider's guide to set its server-only credentials, product or price IDs and webhook verification settings in the matching test or live environment. ## Authorize through entitlements A checkout redirect is not evidence of active access. The server-owned entitlement decides whether a workspace can use a paid capability. Check that entitlement at the action boundary even when the interface already hides the action. ## Verify the lifecycle Use sandbox events to verify activation, updates, cancellation, and webhook handling. Keep repeated events safe and ensure an inactive entitlement blocks paid operations. Native mobile purchases remain a separate RevenueCat integration boundary. ## Setup and configuration The plan catalog lives in src/product-plans.ts and drives the /billing surface and /choose-plan onboarding step. Billing is configured server-side with BILLING_PROVIDER=stripe, STRIPE_SECRET_KEY, the price ids for each plan and interval, and STRIPE_WEBHOOK_SECRET. Lemon Squeezy is available as an alternative adapter. Seat-based plans use the editable seat steppers (src/seat-count.ts); the quantity travels with the checkout session so the billed seats match the workspace. Plans can also declare meteredUsage in src/product-plans.ts so the hourly billing.usage-report job reports usage_counter deltas to Stripe billing meters, while workspace owners set monthly AI budgets and hard per-meter ceilings under Settings → Usage (USAGE_METERS in src/usage-meters.ts). ``` Checkout: POST /api/billing/checkout Portal: POST /api/billing/portal Webhook: POST /api/billing/webhook (signature-verified) ``` ## Implementation workflow A checkout redirect is only a pending intent — access changes when the signed webhook lands and the server-owned entitlement updates. Gate paid actions with the entitlement check at the server boundary (entitlement-gate.tsx / entitlement-check.ts) even when the interface already hides them. The customer portal handles plan changes, cancellation, and payment methods. Keep subscription state readable by workspace members and keep native mobile purchases behind the separate RevenueCat boundary. --- Source: [Database and migrations](https://supastack.dev/docs/web/database.md) # Database and migrations Prepare Postgres and evolve the Drizzle schema with committed migrations. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Install and start Docker Desktop. 2. Copy .env.example to .env in the root for migration tools and to apps/web/.env.local for Next.js (PowerShell: Copy-Item .env.example .env; Copy-Item .env.example apps/web/.env.local. macOS/Linux/Git Bash: cp .env.example .env; cp .env.example apps/web/.env.local). Keep DATABASE_URL the same in both files; the example already matches the local Docker database. 3. From the repository root run docker compose up -d postgres and docker compose ps; wait until postgres reports healthy. 4. Run corepack pnpm --filter @supastack/db db:migrate from the root to apply the migrations already shipped with the kit. Only after editing packages/db/src/schema.ts, run db:generate, inspect the generated SQL, then run db:migrate. Expected: the local Postgres container is healthy and the Web app uses its migrated database. Never point this first run at production. Configure DATABASE_URL for the intended Postgres database in the server environment. Use a separate disposable database for integration acceptance. Never aim test migrations at a production database. The database workspace exports schema definitions and persistence helpers. Review packages/db/src/schema.ts and the migration directory before introducing a table or column. ``` docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate # Only after changing packages/db/src/schema.ts: corepack pnpm --filter @supastack/db db:generate corepack pnpm --filter @supastack/db db:migrate # Optional Compose / Supabase settings in .env: # POSTGRES_PASSWORD=supastack # SUPABASE_ACCESS_TOKEN= # DATABASE_POOLER=transaction ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Choose one copy method for your terminal: # PowerShell: Copy-Item .env.example .env Copy-Item .env.example apps/web/.env.local # macOS/Linux/Git Bash alternative: # cp .env.example .env # cp .env.example apps/web/.env.local docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate ``` ## Implementation workflow Generate a migration after editing the schema, inspect the generated SQL, and commit it with the schema change. Plan how existing records receive values before adding a required column. Include workspaceId on every user-owned record and carry that scope into reads and writes. Use immutable model-price history for AI accounting instead of rewriting prices used by existing usage records. ## Verification and troubleshooting If Docker says it cannot connect to its engine, start Docker Desktop and wait for it to finish loading. If postgres is not healthy, run docker compose logs postgres from the root and fix that error before migrating. If Web says DATABASE_URL is missing, check apps/web/.env.local; if db:migrate cannot connect, check the root .env. If you change databases, update both files. Read the first failed migration statement before retrying; never repeat it against production. Apply migrations to an isolated database and run database-gated tests there. Missing credentials should produce an explicit skipped gate, not an assertion that persistence passed. If migration fails, inspect which statement failed and the current schema before retrying. --- Source: [Public API, keys and webhooks](https://supastack.dev/docs/web/api.md) # Public API, keys and webhooks The versioned REST surface your customers build against. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration packages/api defines the versioned contract: zod schemas for every request and response, the error envelope, and the OpenAPI document served at /api/v1/openapi and rendered at /api/docs. Workspace API keys are created under Settings → Developers; plaintext is shown once, then only the hashed key persists. Keys are prefixed (sk__…), scoped (read by default), and resolved to a workspace on every request. Outbound webhook subscriptions are managed from the same Developers surface. ``` GET /api/v1/workspaces/current GET /api/v1/entitlements GET /api/v1/models/requirements POST /api/v1/ai-tasks GET /api/v1/openapi # machine-readable spec GET /api/docs # rendered reference Authorization: Bearer sk_live_… ``` ## Implementation workflow Authenticate with the Bearer key; the server resolves the workspace from the key, never from a client-supplied id. Keep response shapes versioned — additive changes inside v1, breaking changes behind a new version. For outbound webhooks, subscribe an endpoint URL to the event catalog; deliveries are signed and recorded so consumers can verify and replay them. Use webhook-events.ts for the catalog and webhook-delivery.ts for attempts. ## Verification and troubleshooting Verify a revoked key returns 401, an underscoped key is denied, and the OpenAPI document parses. For webhooks, test signature verification, redelivery after failure, and that event payloads never carry provider secrets. --- Source: [File storage](https://supastack.dev/docs/web/storage.md) # File storage Workspace-scoped uploads through the object-storage contract. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. For a no-credential trial, keep `STORAGE_PROVIDER=memory`. If this checkout has no visible upload control, ask your coding agent to inspect `apps/web/app/api/storage/avatar/route.ts`, `apps/web/app/api/storage/logo/route.ts`, `apps/web/app/api/storage/organization-logo/route.ts`, and the matching settings page before wiring a small sample image. 2. To check persistence, set `STORAGE_PROVIDER=s3`, `STORAGE_REGION`, and `STORAGE_BUCKET` in `apps/web/.env.local`, add server-only test credentials locally, and restart. 3. The image should reload from storage. Memory files disappear when the process restarts; for S3 errors, check bucket, endpoint, and access policy. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Select the storage backend `STORAGE_PROVIDER=memory` is the credential-free default. `local` writes objects to disk under `STORAGE_LOCAL_DIR` (default `./.data/storage`, required explicitly in production) and streams them through HMAC-signed expiring URLs at `/api/storage/local/[...key]` (`STORAGE_LOCAL_SIGNING_SECRET`, falling back to `AUTH_SECRET`) for single-server deployments. `s3` uses `apps/web/src/storage-runtime.ts` to compose the AWS SDK client over the narrow `@supastack/storage` contract, supporting Supabase Storage and MinIO with path-style URLs enabled by default. ``` STORAGE_PROVIDER=s3 STORAGE_REGION=us-east-1 STORAGE_BUCKET=your-product-bucket # Set STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY on the server. # Optional: STORAGE_ENDPOINT, STORAGE_FORCE_PATH_STYLE, STORAGE_PUBLIC_BASE_URL # Self-hosted single-server disk storage: # STORAGE_PROVIDER=local # STORAGE_LOCAL_DIR=./.data/storage # STORAGE_LOCAL_SIGNING_SECRET= # Local MinIO Compose overrides: MINIO_ROOT_USER / MINIO_ROOT_PASSWORD ``` ## Reuse the upload boundaries The shipped routes cover objects, avatars, profile logos and organization logos under `/api/storage/`. They resolve identity/workspace permissions and validate upload shape before storage. Inspect the chosen route's size/type policy rather than accepting arbitrary client object keys. Public URLs are presentation, not authorization. Keep protected content behind the application's verified read boundary and configure bucket/CDN policy for your product's data. ## Test real storage behavior Test allowed/denied uploads, MIME and size rejection, foreign-workspace paths, missing objects and provider failure. Memory mode does not persist across processes; verify storage and retrieval against the selected backend before release. [Supabase Storage setup](https://supastack.dev/docs/web/project-structure.md) [Upload route inventory](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration STORAGE_PROVIDER selects the adapter: memory for tests and local development, s3 for any S3-compatible bucket (STORAGE_BUCKET, STORAGE_ENDPOINT, STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY, STORAGE_PUBLIC_BASE_URL). Credentials stay server-only. packages/storage owns the ObjectStorage contract — putObject/getObject/deleteObject against object locators — plus assertWorkspaceObjectKey and buildWorkspaceObjectKey so every stored object is namespaced by workspace and purpose (avatar, file, logo). ``` Uploads and reads go through authenticated routes: /api/storage/objects /api/storage/avatar /api/storage/logo /api/storage/organization-logo ``` ## Implementation workflow Keep uploads behind the authenticated route layer: resolve the workspace, validate content type and size, build the workspace-scoped key, then store through the adapter. Serve reads the same way unless an object is deliberately public. Avatars and organization logos have dedicated routes with their own validation; arbitrary product files go through /objects with a declared purpose. ## Verification and troubleshooting Verify anonymous uploads are rejected, one workspace cannot read another's object keys, oversized or disallowed content types fail, and a deleted object stops resolving. Use the memory adapter in tests; verify the real bucket through the opted-in smoke path. --- Source: [AI features](https://supastack.dev/docs/web/ai.md) # AI features Chat, tasks, model policy, and bring-your-own-key. ## Try fake AI, then choose a real provider For your first result, follow the Web tutorial's /dashboard task: start the local flow, queue a prompt, run it and view the result. AI_PROVIDER=fake returns a repeatable demonstration response without sending data to an external model or using a paid API key. For real generation, first configure persisted accounts, choose an AI provider and follow AI configuration below. Add a model and its price to the database catalog, choose the permitted models for each alias, and grant the workspace the required access. An alias such as fast is a friendly choice shown to the user; it is not itself an external provider's model ID. A background AI task also needs the configured jobs service. If no model choices are shown, inspect the catalog and alias policy before retrying. If access is denied, inspect the active workspace's entitlement. Keep all provider API keys in the Web server's configuration, not the browser, mobile app or desktop window. [Try the credential-free AI demonstration](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Configure real AI generation](https://supastack.dev/docs/modules/ai-configuration.md) [Add allowed models and prices](https://supastack.dev/docs/web/setup-ai.md) [Configure background task execution](https://supastack.dev/docs/modules/jobs.md) ## Follow the task lifecycle The production task API validates the authenticated workspace and entitlement before persisting a task and dispatching the ai.generate Trigger.dev workflow. The router applies capability requirements and configured model policy. ``` POST /api/ai/tasks → authenticated workspace + entitlement → durable task record → Trigger.dev ai.generate → model routing + generation → usage record + result → transactional completion delivery ``` ## Configure aliases and budgets The user selects an enabled alias: cheap, fast, or smart. The server controls the underlying model IDs, provider credentials, workspace policy, and budget enforcement. Encrypted bring-your-own-key support must preserve the same server-only boundary. ## Keep workflow semantics explicit JobRunner promises enqueue, status, and cancel only. Trigger.dev workflow semantics are explicit, including durable execution behavior. Do not infer retries or scheduling guarantees from the generic contract. The production submission path uses idempotency and saved task recovery to handle ambiguous dispatch. Completion of generation is separate from completion-email delivery. ## Setup and configuration AI_PROVIDER=fake keeps development deterministic. Production providers are configured server-side (OpenAI, Anthropic, or an OpenAI-compatible endpoint) with keys that never enter client bundles. AI_MODEL_POLICY maps the user-facing aliases — cheap, fast, smart — to catalog model ids; an empty alias removes the choice from the interface. The model catalog (ai-model-catalog) carries capability metadata and immutable pricing so usage records stay explainable after prices change. ``` POST /api/ai/tasks → durable task + Trigger.dev dispatch GET /api/ai/tasks/{taskId} → status + result /chat → streaming assistant surface ``` ## Implementation workflow The chat surface keeps threads per workspace member and streams tokens through chat-stream-runtime. Longer generations run as tasks: authorized at the API boundary, executed by the ai.generate workflow, and recoverable through the idempotent submission store. Bring-your-own-key mode stores provider keys as AES-GCM envelopes (CREDENTIAL_ENCRYPTION_KEY, byok-key-vault.ts); decryption happens only inside the server execution path. Usage and estimated cost are recorded per request for workspace budgets. ## Verification and troubleshooting Verify a disabled alias is absent from the interface, entitlement and budget denials happen before dispatch, a retried submission reuses its task, and a stored BYOK key is never returned to the client. Keep real-model acceptance on the documented provider smoke path. --- Source: [Background jobs](https://supastack.dev/docs/web/jobs.md) # Background jobs Durable work through the JobRunner contract and Trigger.dev. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/src/job-handlers.ts and apps/web/src/scheduler-runtime.ts in VS Code. A job is background work; the scheduler decides when to enqueue it. 2. For a local tick, set the same SCHEDULER_TICK_SECRET in apps/web/.env.local and the repository-root .env; the tick script reads the root .env, while the Web endpoint reads its app settings. 3. Start Web with corepack pnpm --filter @supastack/web dev, then run corepack pnpm jobs:tick from the root. Expected: the terminal shows a POST result from localhost:3000. Trigger.dev is a separate provider and needs its own account and server keys. Set JOB_PROVIDER=trigger with server-only TRIGGER_SECRET_KEY and TRIGGER_PROJECT_REF for production task submission. Review apps/web/trigger.config.ts and the ai.generate task in apps/web/trigger before configuring the target project. ``` JOB_PROVIDER=trigger TRIGGER_PROJECT_REF=your-project-reference # Provide TRIGGER_SECRET_KEY through the server secret store. # Local/self-hosted scheduler tick: # SCHEDULER_TICK_SECRET=… # SCHEDULER_TICK_URL=http://localhost:3000 ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Terminal 1 — start the Web app from the monorepo root: corepack pnpm --filter @supastack/web dev # Terminal 2 — run one scheduler tick from the monorepo root: corepack pnpm jobs:tick ``` ## Implementation workflow The shared JobRunner contract promises enqueue, status, and cancel. Trigger.dev owns workflow-specific semantics; do not infer scheduling, retries, or durability guarantees from the generic interface. The durable application task ID connects dispatch, worker execution, usage records, and results. A failed provider-run metadata attachment after confirmed enqueue must not cause another enqueue or invalidate accepted work. ## Verification and troubleshooting If the tick returns 404, confirm the secret exists in both settings files and matches exactly, then restart Web. If the terminal says connection refused, start Web first. If no job ran, check that a schedule is enabled and due; one tick does not make a future schedule run immediately. Verify the deployed task identifier and project match the configured environment. Separate enqueue acceptance from workflow completion and from email delivery. Read docs/production-ai-runtime.md for recovery and safe diagnostics before manually retrying work. --- Source: [Email and mailing](https://supastack.dev/docs/web/email.md) # Email and mailing Transactional templates, preview tooling, and provider adapters. ## Read your first local message, then enable delivery Transactional email means a message triggered by an app action, such as a sign-in code or task result. With the Web server running, keep EMAIL_PROVIDER=local in apps/web/.env.local. Complete database setup, request a sign-in code at /auth, then open /mail-preview and inspect the latest message. A local message appearing here is not an email delivered to your external inbox. To send real messages, choose one email provider, create its account, verify a domain you control and follow its dedicated guide. Put the named API key and sender address in apps/web/.env.local, save and restart the Web server. Request one new sign-in code, then check both the provider's delivery log and the destination inbox, including spam. Domain verification and sender approval can take time. If the message fails, inspect the delivery error and sender verification before requesting repeatedly. The /mail-preview route is development-only and returns 404 in production. Keep marketing outreach separate from this transactional sender so a newsletter campaign cannot disrupt account verification mail. [Choose a mail provider and sender address](https://supastack.dev/docs/modules/email-configuration.md) [Enable Resend delivery](https://supastack.dev/docs/web/setup-email.md) [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/web/aws-ses.md) ## Keep transactional delivery separate The email module handles transactional product messages, including authentication and task completion. Outreach belongs to the consuming product’s own workflow and must remain separate from this transactional path. ## Configure the delivery adapter Use the local adapter for deterministic tests. Configure the selected production adapter and its server-side credentials before enabling external delivery. Never place mail-provider secrets in client configuration or logs. ## Record delivery independently A generated result and its completion email have separate outcomes. Inspect delivery records before claiming a message was sent successfully. Test a failed delivery without treating it as a failed generation or submitting duplicate model work. ## Setup and configuration EMAIL_PROVIDER=local keeps delivery deterministic during development. For production delivery pick one of the configured adapters — Resend (RESEND_API_KEY), AWS SES (AWS_REGION plus access keys), Postmark, SendGrid, Mailgun, generic SMTP, or Brevo — and verify EMAIL_FROM matches a sender identity the provider accepts. All templates live in src/transactional-mail-templates.ts. The dev-only /mail-preview route renders every template so changes are reviewed visually instead of by sending mail. ``` EMAIL_PROVIDER=resend EMAIL_FROM=Your Product ``` ## Implementation workflow Authentication codes, magic links, invitations, billing notices, and task-completion mail all pass through the same transactional sender with the resolved locale's copy. Outreach campaigns are a separate product concern and never share this path. Preview a template in mail-preview before editing copy or layout; keep brand values (name, colors, logo tiles) coming from configuration so derived products restyle without forking templates. ## Verification and troubleshooting Assert on the local adapter's recorded messages in tests — recipient, subject, and rendered body. A failed send must surface as a delivery outcome, not a crashed workflow. Use the opted-in smoke path with REAL_SMOKE_EMAIL_ALLOWED_DOMAIN for real delivery. --- Source: [Notifications](https://supastack.dev/docs/web/notifications.md) # Notifications In-app notices, per-user preferences, and admin announcements. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Sign in as a test member and open Settings → Notifications to review the available channel preferences. 2. Use two test members in one workspace and trigger a local test event, such as an invite, with `EMAIL_PROVIDER=local`. 3. Confirm only eligible members and enabled channels receive a notification in the app or local outbox. Do not send test messages to real members. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Use the product event pipeline `notification-events.ts` maps billing, budget, invite, membership and connected-account events to localized copy and per-member delivery plans. The API and app clients read the signed-in workspace's notifications; preferences control eligible channels. Some events intentionally stay in-app. Connected-account refresh failures, for example, must not expose token detail or trigger unsolicited email. ## Respect recipient preferences Use the existing delivery planner, default preferences and actor-exclusion options rather than independently sending a message to every member. Transactional email still uses the configured provider and records its own delivery outcome. ## Verify recipients and state Test a disabled preference, excluded actor, removed member, unread/read behavior and another workspace. Keep dynamic updates accessible and verify localized messages. [Email delivery](https://supastack.dev/docs/modules/email.md) [Notification dictionaries](https://supastack.dev/docs/web/i18n.md) ## Setup and configuration The notification service (notification-service.ts) stores workspace-scoped in-app records; GET /api/notifications lists them for the signed-in member and the mark-read route updates state. Per-user preference flags gate categories such as product updates and security alerts. Members manage preferences under /settings/notifications. Platform announcements are authored through announcement-actions and delivered through the same pipeline. ``` GET /api/notifications PATCH /api/notifications (mark-read) Emitted on mutations, e.g.: member.left member.role_changed ``` ## Implementation workflow Emit a notification after a successful mutation — never before the write commits. Keep payloads structured (type, workspace, references) so the interface can render and route them without parsing prose. Check the preference flag before delivering a category that can be disabled; security-relevant notices (new session, key changes) should remain mandatory. ## Verification and troubleshooting Verify a disabled preference suppresses delivery, mark-read persists across sessions, another workspace cannot list the records, and announcements reach intended members. Keep notification fan-out off the request's critical path where volume grows. --- Source: [Account and security](https://supastack.dev/docs/web/account.md) # Account and security Profile, sessions, passkeys, data export, and deletion. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Sign in to `/auth` in two test browser sessions, then sign out from one session. 2. Request a protected page from the signed-out session; the server should require sign-in. This guide covers session behavior, not a cross-device session-revocation screen. 3. To inspect your own data export, use `GET /api/account/export`; it must not include another user's or workspace's private records. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Manage active sessions Security settings expose the shipped session and credential controls through the selected auth engine. Revoking a session must invalidate server access, not only clear a local screen. Review `web-settings-security.ts`, the settings panels and auth runtime before extending them. ## Export account data The signed-in `GET /api/account/export` route provides the GDPR data export, including authored content. Keep access tied to the requesting identity and audit the shape whenever you add user-owned records. Never include provider tokens, signing secrets or another workspace's private data. ## Verify account transitions Test current-session sign-out, revoking another session, expiry, reauthentication and export access. Clear the previous account's cached presentation before rendering the next user's workspace. [Privacy and data handling](https://supastack.dev/docs/web/security.md) ## Setup and configuration Account surfaces live under /settings — general, security (password, sessions, passkeys), notifications, appearance, and audit. Each section reads server state and writes through server actions that re-verify the session. Data export (src/data-export.ts) assembles a member's workspace-owned records; account deletion runs through the same ownership boundary. Both exist because privacy obligations are product requirements, not extras. ``` /settings/general name, avatar, email /settings/security password, sessions, passkeys /settings/notifications per-category opt-outs /settings/appearance theme, density, layout ``` ## Implementation workflow Keep security mutations re-authenticated where the provider supports it — password change, key rotation, deletion. Session listing must show the caller's own sessions only, and revocation must actually invalidate the token. Export only data the member owns — reuse the workspace-scoped repositories rather than ad-hoc queries. Deletion ordering matters: revoke sessions and clear dependent records before removing the account row. ## Verification and troubleshooting Verify a revoked session loses access immediately, an export contains the member's records and not another workspace's, and deletion leaves no orphaned ownership rows. Test passkey ceremonies only on HTTPS origins. --- Source: [Content management](https://supastack.dev/docs/web/content.md) # Content management Typed CMS entries with a free lifecycle and a public read boundary. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Sign in as a workspace owner or admin, open `/content`, and use the form to create a post, page, doc, or report with a unique slug. Save it as a draft, then publish it. 2. Confirm the status changes to published; a duplicate slug or member without management access should fail. 3. For a gated test, use `required_entitlement=ai.reference-task`; without a verified grant, the page must show only its teaser. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Create workspace entries The `/content` manager handles `post`, `page`, `doc` and `report` entries. `content_entry` rows have draft/published/archived state, workspace-scoped slugs and authorship. Management actions require the appropriate owner/admin role. This CMS is separate from the kit's typed public docs/blog catalog in `src/resources`. A product can render CMS entries through its own page composition. ## Gate the body on the server `required_entitlement` is nullable: unset entries are ungated. For gated entries, server-only `content-queries.ts` projects readable content or a locked teaser with an empty body. `content-access.ts` owns the projection; never trust a client-supplied entitlement claim. Publishing emits `content.published`; scheduled digests can summarize the publishing team's recent entries. Authored content also participates in account export. ## Verify publishing and access Test draft visibility, publish/archive transitions, duplicate slugs, wrong workspace, free entries and inactive paid grants. Use the paywall block for presentation while the server remains responsible for access. [Paid-content tutorial](https://supastack.dev/docs/web/setup-ai.md) [Content digests](https://supastack.dev/docs/web/project-structure.md) [Paywall presentation](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration The /content workspace surface manages CMS entries — post, page, and doc are the suggested types, but the column accepts any URL-safe product type (lowercase letters, digits, dashes, underscores, up to 64 characters). The content_entry table scopes every row by workspaceId, stamps createdBy, and keeps slugs unique per workspace and type. Entries move through a free draft ↔ published ↔ archived lifecycle — evaluateContentStatusChange in src/content.ts rejects invalid or unchanged transitions, and publishedAt stamps on first publish and survives later unpublishes so readers keep the original date. ``` /content workspace CMS surface src/content.ts input + lifecycle rules src/content-actions.ts member+ mutations content_entry workspaceId, type, slug, status ``` ## Implementation workflow Members and above author entries — every mutation resolves the session, the active workspace, and at least a member role through requireWorkspaceWriter (workspace-access.ts), so guests stay read-only. normalizeContentEntryInput validates type, a 1–255 character title, and a URL-safe slug before any write. listPublishedContentEntries is the deliberate public read boundary: it takes an explicit workspaceId, needs no session, and returns published rows only — the path storefront pages, blogs, and docs use to render workspace content. Publishing emits a content.published outbound webhook, and authored entries join the GDPR data export. ## Verification and troubleshooting Exercise a draft → publish → archive round trip, a duplicate slug inside the same type, and a cross-workspace read. Confirm the public boundary never returns drafts or another workspace's rows, and that content.published fires once per publish action. --- Source: [Commerce](https://supastack.dev/docs/web/commerce.md) # Commerce Workspace products and orders on a forward-only lifecycle. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. As a workspace owner or admin, open `/commerce` and use the product form to create a draft with a name, currency, integer minor-unit price, and optional tracked stock (for USD, `1299` means $12.99). 2. Activate it and create a local test order; confirm the server calculates the total and stores the product snapshot. 3. Do not connect a live payment provider or charge a real buyer. If an order cannot advance, check available stock and the documented order state before retrying. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Manage products and inventory `/commerce` provides owner/admin management for workspace products. Product states are draft/active/archived; prices use minor currency units and inventory is optionally tracked. Shared persistence lives in `packages/db`. This is a product-building module. It does not include purchasing this kit, source-repository delivery or seller license fulfillment. ## Follow the order lifecycle Orders advance pending → paid → fulfilled, with cancellation/restocking and refund behavior. The server computes totals and stores item product-name/unit-price snapshots so edits to a product do not rewrite historical orders. `commerce.order_*` events support downstream integrations. Connect a real product payment flow deliberately. An admin state transition alone does not verify a payment provider charge or refund. ## Test stock and state transitions Verify insufficient inventory, concurrent orders, client-price tampering, invalid backward transitions, cancellation restocking, refund behavior and cross-workspace access. [Outbound commerce events](https://supastack.dev/docs/web/project-structure.md) [Product grid block](https://supastack.dev/docs/web/project-structure.md) [Records table block](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration The /commerce surface manages a product catalogue and orders for the active workspace. Products carry a name, slug, integer-cent price, three-letter currency, optional image URL, and optional tracked inventory; orders store line items in commerce_order_item with totals computed on the server. Order status moves forward only — pending → paid → fulfilled, with cancel before payment and refund after. evaluateOrderStatusChange rejects invalid, unchanged, or forbidden transitions, so a cancelled or refunded order can never reopen. ``` /commerce owner/admin console src/commerce.ts money math + lifecycle src/commerce-actions.ts owner/admin mutations commerce_product/order/order_item ``` ## Implementation workflow Only owners and admins write — every action resolves the session and workspace through requireWorkspaceManager. createCommerceOrderAction validates line items (whole quantities of 1–9999, duplicate products merged), restricts lines to active products, recomputes totals server-side, and keeps an order to a single currency. Tracked inventory decrements when an order is created and restocks on cancel; a blank inventory field means untracked. Order creation and every status change emit commerce.order_created and commerce.order_status_changed webhooks plus audit rows. ## Verification and troubleshooting Verify a member below admin cannot create products or move orders, backward and sideways transitions are rejected, and a paid order cannot be cancelled. Check that totals come from the server — never trust a client-supplied amount — and that stock returns after a cancel. --- Source: [Marketing pages](https://supastack.dev/docs/web/marketing-site.md) # Marketing pages Landing sections, legal pages, FAQ, changelog, and the site config. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Open `apps/web/src/marketing/site-config.ts` and change the product name, tagline, navigation labels/URLs, and support address to values you own. 2. Replace `apps/web/public/og.png` and update its alt text and dimensions in `apps/web/src/marketing/seo.tsx` as one change. 3. Refresh the home, FAQ, footer, and social preview. If old text remains, search this repo for the old name before publishing. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Update the site configuration `apps/web/src/marketing/site-config.ts` owns brand copy, navigation/footer links, FAQ and optional repository/docs destinations. Update this before scattering product names across components. The kit's own chrome uses this config; the copyable chrome blocks accept product props. ## Replace the asset family together The shared four-tile mark has white top-left/bottom-right and gray top-right/bottom-left tiles with thin outlines. Keep header marks transparent; app icons/favicon use the rounded black backing. Derived products can replace the mark and its associated app/desktop assets together. The shared public social card is `/og.png`; update its descriptor in `marketing/seo.tsx` with the dimensions and alt text. Blog article cards use their separate Next image route. ## Check every public surface Verify header/footer, contact, FAQ, legal pages, localized home copy, titles and social cards. Choose your product's marketing theme mode and only ship support/contact links that work. [Product styles](https://supastack.dev/docs/web/theming.md) [Legal page scaffold](https://supastack.dev/docs/web/project-structure.md) [Metadata and social cards](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration The marketing surface is wired to typed data rather than hardcoded JSX — hero, feature sections, pricing, FAQ, changelog, showcase, contact, and legal pages all read copy and structure from the site configuration and content files. MARKETING_THEME_MODE (src/theme/marketing-theme.ts) decides whether marketing stays on the kit's locked dark brand, inherits the visitor's theme pack, or pins a specific pack. Derived products choose their own mode. ``` src/marketing/ branded chrome and sections src/kit/blocks/ copyable token-only section patterns marketing/ hero, features, product grid, pricing, FAQ, CTA app/ stat grid, data table, empty state, choice pills chrome/ site header, footer, prose page MARKETING_THEME_MODE = "locked" | "inherit" | "" ``` ## Implementation workflow Compose pages from the block library first; write a new block in the same token-only style when a pattern is missing, and register it in src/kit/blocks.ts. Keep marketing components out of product surfaces — the two chrome systems stay separate. Localized copy flows through the dictionary and site-content overlays; add keys there rather than forking components per language. ## Verification and troubleshooting Check a marketing page at 320px and desktop, with visible keyboard focus, under at least one light and one dark pack, and with reduced motion. If a marketing claim references a capability, make sure the linked doc page exists. --- Source: [SEO and sharing](https://supastack.dev/docs/web/seo.md) # SEO and sharing Metadata, sitemap, robots, and social images. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration Route-level metadata comes from the Next.js metadata API — pages export a metadata object or generateMetadata, resolving titles and descriptions from the locale dictionaries so search results stay localized. sitemap.ts and robots.ts generate the crawlable surface; the Open Graph image and icon assets carry the social preview. Keep private routes — settings, billing, admin, API — out of the sitemap and disallowed in robots. ``` app/sitemap.ts public routes only app/robots.ts disallow private surfaces // per page export const metadata = { title, description }; ``` ## Implementation workflow Give every public page a unique title and description; never index search results, filtered views, or authenticated surfaces. Canonical URLs stay un-prefixed — the locale layer already redirects the default locale to the canonical path. When you add a public route, add it to the sitemap in the same change. Keep OG imagery on brand tokens instead of baked screenshots so restyles stay consistent. ## Verification and troubleshooting Fetch /robots.txt and /sitemap.xml on a preview deploy, validate the sitemap parses, and share-check one page — title, description, image. Confirm a noindex header or meta on search-result pages. --- Source: [Product theme](https://supastack.dev/docs/web/theming.md) # Product theme Customize kit product chrome for your SaaS — without editing the marketing brand theme. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. In VS Code open apps/web/app/app-theme.css for signed-in product screens and apps/web/src/theme/catalog.ts to add a color pack. Do not edit apps/web/app/theme.css for product colors; it controls the marketing brand. 2. From the repository root run corepack pnpm --filter @supastack/web dev, then visit http://localhost:3000/settings/appearance. 3. Change a token in your local branch and refresh the preview. Expected: the dashboard controls update while the homepage keeps its selected marketing theme. Marketing brand and product theme are separate files. Edit apps/web/app/app-theme.css for dashboard, auth, settings, billing, chat, and org chrome. Add color packs in apps/web/src/theme/catalog.ts. Extend density, radius, font, and layout axes in apps/web/src/theme/presets.ts plus matching html[data-*] rules in app-theme.css. Pick the marketing identity in apps/web/src/theme/marketing-theme.ts (MARKETING_THEME_MODE: locked | inherit | a pack id) instead of editing apps/web/app/theme.css — that file's lock via .marketing-theme:not(.app-theme):not([data-theme]):not([data-marketing-theme="inherit"]) keeps the supastack Obsidian homepage brand by default. At runtime, Settings → Appearance → ThemePicker stores color, density, radius, font, and layout in browser cookie + localStorage so theme-boot.ts restores all axes before first paint, and syncs the signed-in user's full appearance bundle across browsers through user_appearance (GET/PUT /api/account/appearance). The same page mounts the kit UI gallery so packs and presets live-update buttons, inputs, cards, badges, and dialogs. Design detail and copy-paste examples live in docs/design/kit-product-theme.md and docs/design/kit-ui-gallery.md. ``` /* Edit product chrome here — not theme.css */ .app-theme { --radius-card: 14px; background: var(--page); color: var(--text); } .app-theme .button-primary { background: var(--accent); color: var(--accent-ink); } /* catalog.ts — append a ThemeDefinition with every THEME_TOKEN_NAMES entry */ { id: "coral", name: "Coral", colorScheme: "light", tokens: { "--page": "#fff7f4", "--accent": "#e06b4f" /* …full token set… */ } } ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev # In a second terminal, after changing a theme token: corepack pnpm test -- apps/web/src/theme/contrast.test.ts ``` ## Implementation workflow Prefer var(--page|--section|--raised|--text|--muted|--border|--accent|…) in app consumers (app-theme.css, globals.css aliases, WorkspaceShell routes). Do not reintroduce teal/pale hex shells or hardcode Obsidian hex into product UI. When adding a pack, keep contrast.test.ts green. When adding a preset id, update both presets.ts and the html[data-density|data-radius|data-font|data-layout] remaps in app-theme.css so ThemePicker and FOUC boot stay aligned. Preserve the shared four-tile brand geometry in marketing/brand-geometry.ts for marketing surfaces. Header marks stay transparent; favicon/app icons keep the rounded black backing. Pure marketing pages keep looking Obsidian while MARKETING_THEME_MODE stays "locked" (the kit default); products may pin another pack or inherit the visitor pick. ## Verification and troubleshooting If a color change affects the marketing homepage, check that you edited app-theme.css and review apps/web/src/theme/marketing-theme.ts. If text becomes hard to read, restore the previous value and run corepack pnpm test -- apps/web/src/theme/contrast.test.ts from the root before trying again. After editing kit theme tokens or packs, verify ThemePicker on /settings/appearance updates the kit UI gallery plus /dashboard, /auth, /billing, /chat, and org shells. Confirm / stays on the Obsidian marketing lock (unless MARKETING_THEME_MODE says otherwise). Run theme unit contracts (catalog, presets, contrast, theme-ownership, kit-ui-gallery) and application-theme Playwright when available. Check keyboard focus, contrast, 320px, and reduced motion. --- Source: [UI kit and blocks](https://supastack.dev/docs/web/ui-kit.md) # UI kit and blocks Shared components, design tokens, and the block catalog. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration packages/ui-web holds the shared product components (buttons, inputs, cards, badges, dialogs, shell pieces) and packages/design-tokens the token source that app-theme.css maps into CSS variables. The /kit-blocks route mounts the block catalog — composed sections you can lift into your product. apps/mobile consumes packages/ui-native for the equivalent native primitives, so surface styling stays per-platform while contracts stay shared. ``` # Preview shared blocks and gallery corepack pnpm --filter @supastack/web dev # Then open http://localhost:3000/kit-blocks in your browser # Theme tokens used by components var(--page) var(--section) var(--raised) var(--text) var(--muted) var(--border) var(--accent) var(--accent-ink) var(--radius-*) ``` ## Implementation workflow Compose screens from the shared components first; reach for a custom element only when the kit cannot express the pattern. New shared pieces belong in ui-web with token-driven styling so every theme pack applies automatically. Keep marketing components (src/marketing) out of product surfaces and vice versa — the two chrome systems are intentionally separate. ## Verification and troubleshooting Verify a new component against every density/radius/font preset and at least one light and one dark pack. Check keyboard focus, 320px layout, and reduced motion; the kit gallery on /settings/appearance is the fastest sweep. --- Source: [Locales and copy](https://supastack.dev/docs/web/i18n.md) # Locales and copy Use the four-locale dictionary scaffold for app chrome, settings labels, mail, and notification events. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open packages/i18n/src/index.ts in VS Code to see the supported language names and locale helpers; locate the matching dictionary files before changing product copy. 2. Add the same new key and translation to English, German, Spanish, and French dictionaries. 3. From the repository root run corepack pnpm test -- packages/i18n/src/index.test.ts and start Web with corepack pnpm --filter @supastack/web dev. Expected: the language switcher changes the visible text while preserving the current page path. Locales live in @supastack/i18n with SUPPORTED_LOCALES, LOCALE_COOKIE, resolveLocale, getDictionary, and localePath. Proxy sets x-supastack-locale and handles canonical English redirects; LanguageSwitcher updates the cookie and preserves active sub-paths. Key app routes (/dashboard, /settings, /billing, /chat) work directly under /[locale] prefixes alongside the soft /{locale} marketing home. Dictionaries cover marketing nav, settings section labels, organization settings chrome, transactional mail (invite/OTP/magic-link/AI/billing), notification preference labels, and in-app notification event copy. ``` import { getDictionary, resolveLocale, LOCALE_COOKIE } from "@supastack/i18n"; const locale = resolveLocale([ cookieStore.get(LOCALE_COOKIE)?.value, headers.get("x-supastack-locale"), headers.get("accept-language"), ]); const copy = getDictionary(locale); ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm test -- packages/i18n/src/index.test.ts corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Pass locale into server-rendered settings/org pages and into notificationCopyForEvent / auth mail delivery options when available. Keep English as DEFAULT_LOCALE and avoid inventing marketing claims outside dictionary keys. Transactional mail stays separate from outreach. Expand dictionaries before nesting every marketing route under [locale]. ## Verification and troubleshooting If English appears for one phrase, check that the key exists in all four dictionaries and that the page asks for the same key. If switching languages returns to the homepage, preserve the current path when building the locale link. Keep email copy on the transactional email path; do not reuse it as bulk outreach. Run packages/i18n Vitest, proxy tests, and locale-app-routes tests. Spot-check LanguageSwitcher on / and localized Settings/org/dashboard/billing/chat headings when the locale prefix is de/es/fr. --- Source: [Writing blog articles](https://supastack.dev/docs/web/blog-content.md) # Writing blog articles Publish local, typed articles with working categories and guide links. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. In VS Code open apps/web/src/resources/content.ts and copy a complete nearby article as your starting point; article copy lives in the repository, not in a publishing dashboard. 2. Give the article a new slug, title, category, date, sections, and an existing guide slug. 3. From the repository root run corepack pnpm --filter @supastack/web dev and open http://localhost:3000/blog/. Expected: the article and its related-guide link load locally before you share the URL. Blog content is stored in src/resources/content.ts and blog-posts.ts. This is repository-managed content; there is no CMS publishing service. Each Post needs a unique slug, title, description, category, date, related guide, and sections. ``` { slug: 'your-article', title: 'A specific reader question', category: 'Guides', date: '2026-09-07', guide: 'web/local-setup', sections: [{ id: 'start', title: 'Start here', paragraphs: ['Your original text.'] }] } ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev # Replace with the article slug: # http://localhost:3000/blog/ corepack pnpm test -- apps/web/src/resources/content.test.ts ``` ## Implementation workflow Follow a complete existing Post object for the remaining type fields. Keep the slug stable after publication so shared URLs continue to work. Headings need unique section IDs for the table of contents and anchor links. Categories derive from the article catalogue. The index uses URL-based filtering, and article routes generate metadata from the content. The guide value must match an existing documentation slug; related content should lead to a useful next step. ## Verification and troubleshooting If the article returns 404, check that its slug is unique and that it was added to the article list in apps/web/src/resources/content.ts. If the table of contents jumps nowhere, give every section a unique id. Run corepack pnpm test -- apps/web/src/resources/content.test.ts from the root to see content validation errors. Run the content tests, open the article route, follow its related guide, and check the filtered index. Check dates and claims against the source material. Do not publish placeholder links or claim that an integration has been verified merely because it appears in an article. --- Source: [Product documentation](https://supastack.dev/docs/web/docs-site.md) # Product documentation The dedicated docs app that ships with the kit. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration apps/docs is the buyer-facing documentation surface for the product you build — separate from this kit documentation. It renders your product's own guides with navigation, per-page anchors, and search, and runs as its own workspace app on port 3001 in development. Keep its content typed and versioned in the repository alongside the features it describes; a doc that drifts from the code is worse than a missing page. ``` apps/docs/ Product documentation app content/ Guide sources and nav order app/ Routes, layout, search ``` ## Implementation workflow Model your product's docs on the kit's own structure: a getting-started path, one page per feature area, and reference pages for configuration and APIs. Reuse the kit's guide format so tooling (search index, copy-as-Markdown) keeps working. Link tasks to pages: when you ship a feature, update the matching guide in the same change. ## Verification and troubleshooting Build the docs workspace, click through every nav entry, run a search for each new term you introduce, and check anchor links after renames. Treat broken doc links like broken routes. --- Source: [Feature flags and announcements](https://supastack.dev/docs/web/feature-flags.md) # Feature flags and announcements Roll out dark, gate per workspace, and broadcast to members. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Sign in as a platform admin and open `/admin/flags`; create or select one test flag and set it explicitly to enabled, then disabled. 2. Refresh the product surface that reads the flag and confirm its presentation changes. 3. Roles and entitlements must still protect server actions when a flag is enabled. If the API remains accessible while disabled, add a server-side flag check where intended. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Configure flags in Admin `/admin/flags` and `feature-flags.ts` provide the kit's flag management/evaluation path. Flag configuration belongs to the platform admin; a product UI can react to an allowed feature without gaining authority to change the flag. ## Keep authorization separate A flag enables product behavior, while roles and entitlements authorize a caller. Check both where required. A disabled navigation link does not prevent a direct API call. ## Verify enabled and disabled behavior Test defaults, missing flags, updates, role denial and flag-gated server actions. Preserve workspace access checks when adding rollout logic. [Platform roles](https://supastack.dev/docs/web/admin.md) [Paid access](https://supastack.dev/docs/web/setup-payments.md) ## Setup and configuration feature-flag-runtime resolves flags for a caller context: unconfigured flags default off, and per-flag overrides target specific workspaces or users. A missing flag can never silently enable a surface. Platform announcements (announcement-actions) broadcast in-app notices through the same notification surface members already read — listActiveAnnouncements feeds the banner, dismissAnnouncementAction records the dismissal. ``` await isFeatureEnabled(flag, { workspaceId }) await resolveFeatureFlags(context) listActiveAnnouncements() dismissAnnouncementAction(formData) ``` ## Implementation workflow Ship code dark: merge behind a flag, enable it for your own workspace, then widen. Remove the flag once the surface is permanent — a stale flag is a second source of truth. Check the flag at the server boundary AND the navigation: a hidden link with an open route is not a gated feature. Keep announcements off the transactional email path — they are product news, not receipts. ## Verification and troubleshooting Verify a disabled flag hides nav, route, and action end-to-end; an override resolves only for its target; and a cleared flag returns to the default. Test announcement delivery reaches the intended workspaces and that dismissal persists. --- Source: [Security and privacy](https://supastack.dev/docs/web/security.md) # Security and privacy Rate limits, secrets, data export, and the audit trail. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. In the server action or route that reads a user-owned record, resolve the signed-in user's membership and active `workspaceId` before querying. 2. Ask your agent to add one allowed-member case and one foreign-workspace denial for the exact record path; provide the roles and fields involved. 3. The first request should succeed and the second should return no record/access denied. If changing only the button hides access but direct requests still work, the server boundary is missing. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Resolve membership before data The session chooses identity; the server resolves active workspace and verifies membership/role. Every user-owned record uses `workspaceId`. A client field, URL ID or model-selected argument cannot replace that ownership check. Platform marketing assets and platform admin records have explicit platform scope; do not invent a workspace for anonymous waitlist entries. ## Carry the boundary through asynchronous work Tasks, jobs, tool context, API keys, content, storage and outbound events inherit the authenticated workspace. Check resource IDs together with ownership. Allowing a valid ID from another workspace is still an authorization failure. ## Write denial tests first Pair every successful mutation/read with anonymous, foreign-workspace and insufficient-role tests. Never expose secrets while diagnosing a forbidden result. [Workspace contracts](https://supastack.dev/docs/modules/workspaces.md) [Protected-page recipe](https://supastack.dev/docs/web/first-feature.md) [Server-injected tool context](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration rate-limit-runtime provides the shared limiter wired into auth, checkout, API, and submission routes — extend it to new abuse-sensitive endpoints rather than inventing per-route counters. Session configuration, HTTPS-only cookies, and provider secrets stay server-side. Workspace audit records (audit-runtime.ts) capture security-relevant changes — provider configuration, member and role updates — and are readable under /settings/audit. ``` # Data subject export src/data-export.ts — assembles a member's workspace-owned records for account export ``` ## Implementation workflow Keep credentials out of logs, error payloads, analytics properties, and audit values — the observability layer redacts known keys, but free-form text is the author's responsibility. Resolve authorization on the server for every read and write; interface state is presentation, not proof. Account data export and deletion requests run through the account boundary so the same ownership rules apply. Store only the data the product needs. ## Verification and troubleshooting Exercise the limiter until it returns a retryable 429, verify audit rows appear for the mutations that should produce them, and export a test member's data. Probe one cross-workspace read per release — the cheapest regression that matters. --- Source: [Spam protection](https://supastack.dev/docs/web/captcha.md) # Spam protection Env-switched Turnstile checks on public forms and auth endpoints. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Create a Turnstile test widget for the exact local/deployed hostname. In `apps/web/.env.local`, set `CAPTCHA_PROVIDER=turnstile` and `NEXT_PUBLIC_TURNSTILE_SITE_KEY`; put `TURNSTILE_SECRET_KEY` only in the server secret store. 2. Restart and submit a test contact form. 3. Expect the challenge and a successful valid-token response. If every request fails, compare hostname and key pair; never expose the secret key. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Enable both keys `CAPTCHA_PROVIDER=off` is the default. Select `turnstile`, configure the matching hostname in your Turnstile application and provide both keys. `NEXT_PUBLIC_TURNSTILE_SITE_KEY` is public; `TURNSTILE_SECRET_KEY` is used only by the server verifier. ``` CAPTCHA_PROVIDER=turnstile NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-site-key # TURNSTILE_SECRET_KEY belongs in the server secret store. ``` ## Preserve the protected surfaces The gate covers contact/newsletter forms and Better Auth sign-in, sign-up, OTP and password-reset credential endpoints. It complements validation/rate limiting; it is not a substitute for sessions or workspace authorization. ## Verify rejection and recovery Test a valid token, missing/expired/invalid token, verifier failure and a retry after the client widget resets. Keep actionable human-check copy and keyboard access; inspect the chosen auth engine's coverage before switching providers. [Protected forms](https://supastack.dev/docs/web/project-structure.md) [Auth engine differences](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration CAPTCHA_PROVIDER selects the verifier: off (the default, so local development stays credential-free) or turnstile. Enabling turnstile requires TURNSTILE_SECRET_KEY on the server and NEXT_PUBLIC_TURNSTILE_SITE_KEY for the client widget — the config schema fails startup if either is missing. apps/web/src/captcha.ts composes one process-wide verifier that posts the token to Cloudflare's siteverify endpoint with a ten-second timeout and fails closed — an unverifiable token is never treated as proof of humanity. ``` CAPTCHA_PROVIDER=turnstile TURNSTILE_SECRET_KEY=… # server-only NEXT_PUBLIC_TURNSTILE_SITE_KEY=… Guarded: sign-in/up, OTP send, reset, contact + newsletter forms ``` ## Implementation workflow CaptchaWidget (src/captcha-widget.tsx) renders the widget and injects the cf-turnstile-response field into the parent form, while auth client calls carry the token in the x-captcha-response header. The contact and newsletter actions verify before writing — a failed check returns status=captcha instead of creating a lead. packages/auth mounts better-auth's captcha plugin when the provider is on; CAPTCHA_ENDPOINTS covers the credential-entry and token-issuing routes — sign-in, sign-up, OTP send, password reset. Consuming endpoints such as check-verification-otp stay unlisted: the OTP or token itself is the proof there. ## Verification and troubleshooting Submit each protected form with a missing token and a malformed one — both must fail closed. Verify an unreachable siteverify rejects rather than passes, provider=off leaves local development open, and the secret never reaches client bundles or logs. --- Source: [Admin console](https://supastack.dev/docs/web/admin.md) # Admin console Platform inspection, announcements, and feature flags. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Before creating a local test account, add your own address to the server-only `ADMIN_EMAILS` list and restart the app. 2. Sign up on the migrated test database and open `/admin`; inspect the available user and organization pages. 3. This grants platform admin, not workspace ownership. If access is denied, verify the exact email and server configuration; do not elevate arbitrary users or test against production. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Create the admin boundary `ADMIN_EMAILS` is a server-only comma-separated bootstrap list. Listed users receive the platform admin role on signup. Without it, grant access through an authorized database/admin path; do not make the first arbitrary visitor an admin. Platform admin is distinct from workspace owner/admin. `/admin` and its user, organization, flag, announcement and waitlist pages check the platform role. ## Use the implemented controls The default Better Auth engine provides admin user management, bans and impersonation. The app composes platform views with audit trails. Restrict impersonation to verified admin access, make its state visible and restore the original admin session on exit. Auth.js does not automatically inherit Better Auth admin plugins. Review `admin.ts`, `admin-actions.ts` and auth-provider behavior before switching engines. ## Verify privileged actions Test non-admin denial, admin access, banned-user sessions, impersonation start/stop and the actions you enable. Keep all admin routes outside public indexing. [Feature flags](https://supastack.dev/docs/web/feature-flags.md) [Announcements](https://supastack.dev/docs/web/project-structure.md) [Waitlist administration](https://supastack.dev/docs/web/setup-ai.md) ## Setup and configuration The /admin surface is the operator's inspection view over workspaces, members, and product state. ADMIN_EMAILS (or the admin plugin configuration in the consuming product) decides who reaches it; every admin route re-checks authorization on the server. Feature flags (feature-flag-runtime.ts) default-off anything unconfigured — a missing flag can never silently enable a surface — and per-flag overrides live in the featureFlagOverride table. ``` /admin operator dashboard announcement-actions.ts broadcast in-app notices resolveFeatureFlags(...) bulk flag resolution for a caller context ``` ## Implementation workflow Use admin actions for inspection and support operations (workspace lookup, member management, announcement broadcast). Keep mutations audited and authorization checked at the action boundary, not the nav. Roll features out behind flags: ship code dark, enable per workspace or globally, and remove the flag once the surface is permanent. Platform impersonation stays frozen unless the product's PRODUCT.md explicitly authorizes it. ## Verification and troubleshooting Verify a non-admin is denied, a disabled flag hides its surface end-to-end (nav, route, action), an override resolves only for its target, and announcements reach the intended audience. Admin tests should cover the denied path first. --- Source: [Testing strategy](https://supastack.dev/docs/web/testing.md) # Testing strategy Understand what deterministic, browser, database, and provider checks establish. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open the repository-root package.json to see the check commands. 2. From that root folder run corepack pnpm test for automated unit tests, then corepack pnpm --filter @supastack/web typecheck for Web TypeScript checks. 3. For browser behavior, start the Web app and run corepack pnpm test:e2e. Expected: each command reports its own result; a unit test does not prove that a real database, payment provider, email service, or phone works. The default test suite uses deterministic fake/local adapters. Browser tests cover user interactions; database acceptance and external provider smoke are separate gates. Run checks relevant to the changed behavior before broadening the scope. ``` corepack pnpm test corepack pnpm check corepack pnpm build corepack pnpm test:e2e corepack pnpm test:e2e:production ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm test corepack pnpm --filter @supastack/web typecheck corepack pnpm test:e2e ``` ## Implementation workflow For behavior changes, write a failing acceptance test first. Pair successful access with denied anonymous and cross-workspace requests. For forms and task flows, verify loading, error, and recovery behavior as well as the happy path. An intercepted browser response proves that the interface responds to that fixture. It does not prove authentication persistence, payment processing, provider delivery, or a physical device callback. ## Verification and troubleshooting If a database test says it was skipped, set up its isolated test database before reporting database coverage. If a Web tab is at port 3001, it is the separate apps/docs server; the kit Web app starts at port 3000. If Playwright fails on its first Windows run, let browser installation finish and rerun the failing command; never turn off system-wide security to work around it. If a database-gated test skips, configure its isolated test database before claiming that gate passed. If a browser runner emits passing assertions but hangs during teardown, report both facts; a stuck process is not a clean successful command. On Windows, an initial browser run may time out while Defender scans newly installed Playwright browsers or dependencies. Let installation finish, rerun the failed suite with corepack pnpm test:e2e or corepack pnpm test:e2e:production, and inspect its trace and Defender Protection History if it fails again. Leave system-wide protection enabled. The local browser suite owns port 3100 and refuses to reuse a process already listening there. Set PLAYWRIGHT_PORT to a free port before corepack pnpm test:e2e if another app owns 3100; this prevents tests from silently targeting another project. --- Source: [Deployment checklist](https://supastack.dev/docs/web/deployment.md) # Deployment checklist Prepare the production dependencies and verify the flow you intend to ship. ## Prepare a public app in this order Deployment means running your app on a hosting service so other people can visit it. localhost is accessible only in your development environment. First finish real database-backed sign-in locally, choose a hosting guide below, create a separate test database, and configure the host's environment settings with the actual public HTTPS app URL. Keep local demonstration settings separate from the public app. In a second terminal at the repository root, run the Web checks below. A successful build creates a production version on your computer; it does not upload or publish it. Do not confuse corepack pnpm --filter @supastack/web start with deployment: start runs the previously built version locally. Run it only after stopping the development server on port 3000. Follow the chosen host's guide for migration, environment and launch steps. Test sign-up, an email code, saved account data after a restart and a paid feature in the provider's sandbox before opening sales. A mobile or desktop client must point to this running HTTPS backend. Releasing a native app is a separate step documented in its platform's distribution guide. [Deploy to Google Cloud](https://supastack.dev/docs/web/deploy-google-cloud.md) [Deploy on a server with Coolify](https://supastack.dev/docs/web/project-structure.md) [Prepare database-backed sign-in](https://supastack.dev/docs/web/database.md) ``` corepack pnpm --filter @supastack/web typecheck corepack pnpm --filter @supastack/web build ``` ## Prepare the services Create the Postgres database, apply the repository's migrations and enter your public app URL and authentication settings in the hosting dashboard. Choose one supported Web billing provider and one transactional email provider for the features you enable. Configure Trigger.dev when your product uses its background workflows; you do not need every integration to publish a basic app. Configure the immutable model/price catalog and workspace AI policy before accepting production AI tasks. An empty alias policy should leave those choices unavailable. ## Build and deploy the web application Run the Web commands below from the repository root and resolve any error before continuing. typecheck checks the types, test runs the Web tests, and build creates production files locally. Follow your selected hosting guide to upload and run them with its server-side environment settings. These commands do not deploy the app. ``` corepack pnpm --filter @supastack/web typecheck corepack pnpm --filter @supastack/web test corepack pnpm --filter @supastack/web build ``` ## Verify the deployed boundaries Verify sign-in and persistence against the deployed database; test workspace isolation; confirm Stripe sandbox webhook processing and entitlement changes; and run a background task through result persistence and transactional delivery. Read docs/real-provider-smoke.md and docs/production-ai-runtime.md in the repository for the explicit provider acceptance process. A successful build or intercepted browser test does not establish these external outcomes. --- Source: [Troubleshooting](https://supastack.dev/docs/web/troubleshooting.md) # Troubleshooting Diagnose setup problems at the boundary that actually failed. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Copy the first useful error line and the command that produced it; do not paste secret values. 2. From the repository root run corepack pnpm --filter @supastack/web typecheck for Web type errors, or corepack pnpm test for the automated suite. 3. For a browser issue, start Web with corepack pnpm --filter @supastack/web dev and reproduce it at http://localhost:3000. Expected: you can tell whether the failure is in setup, code, database, provider, or the browser before changing settings. Read the failing command and first relevant error before changing configuration. Determine whether the failure is dependency installation, compilation, environment validation, database access, provider dispatch, or browser behavior. ``` corepack pnpm --filter @supastack/web typecheck corepack pnpm --filter @supastack/web build corepack pnpm test ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web typecheck corepack pnpm test ``` ## First-run errors, decoded The first run fails in a small set of ways. Match the first red error line to a row — later errors are usually consequences of the first, so fix the top one before anything else. | Symptom | Likely cause | First fix | |---|---|---| | node, corepack or pnpm is not recognized / command not found | The tool is not installed, or the terminal was open before installation finished | Install it (or reopen the terminal), then retry node --version | | No package.json found / ENOENT package.json | The terminal is in the wrong folder | cd into the repository root — the folder that contains package.json — and retry | | EADDRINUSE / port 3000 is already in use | Another dev server still holds the port | Find that server's terminal and press Ctrl + C, then start again | | ECONNREFUSED to localhost:5432 or a database connection error | Postgres is not running | Run docker compose up -d postgres, wait for healthy, then retry | | Env validation error naming a missing key | apps/web/.env.local is missing or has an empty required value | Copy the named key from .env.example, give it a value, save and restart the server | | corepack or pnpm missing after corepack enable | No pnpm shim is on PATH | Use corepack pnpm ... directly — it never needs a shim; on Windows use corepack.cmd | | PowerShell says a .ps1 script cannot run / is blocked | PowerShell's script policy blocks the shim | Use the .cmd form — corepack.cmd pnpm install — no policy change needed | ## Implementation workflow For missing API access, verify the server is running and the configured origin matches the client. For unavailable AI choices, inspect AI_MODEL_POLICY and enabled catalogue rows. For an uncertain queued task, use the saved recovery flow instead of starting another submission. For browser layout issues, inspect computed styles and reproduce the exact viewport. Product chrome tokens live in app-theme.css (packs from catalog.ts); marketing brand lock lives in theme.css. Component overrides can still win through specificity. Check the rendered radius rather than only the token declaration. ## Verification and troubleshooting If the page is on :3001, open :3000 for the Web app; :3001 belongs to the separate apps/docs server. If a test asks for database access, use an isolated development database and check both .env locations. If the same error remains, include the command and sanitized first error line when asking for help; never disable access checks or print secrets to logs. Do not solve a failing check by disabling authorization, logging secrets, or treating skipped infrastructure checks as passed. Capture a small reproduction and the relevant non-secret configuration. Re-run the failing check after the fix, then the affected regression tests. --- Source: [How to use these docs](https://supastack.dev/docs/web/using-the-docs.md) # How to use these docs Find your way around the documentation — tabs, groups, search and section links — and hand any guide to a coding agent with the built-in copy tools. ## How the pages are organized These docs are the manual for the kit you downloaded. The surface picker above the guide list — Web, Mobile and Desktop — holds the guides for each surface: the browser app, the phone app and the desktop window. Start with Web; it covers the server every surface talks to. Each surface opens on its own five-minute quickstart. The left column lists guides inside named groups such as Getting started and Set up providers; select a group heading to open or close its list, and the current page stays highlighted. The right column, On this page, lists the sections of the guide you are reading — select one to jump straight to it. To find a guide by a word anywhere inside it, such as a file or setting name, use Search at the top of the left column or press Ctrl + K (⌘K on a Mac). At the bottom of a guide, a Next card — and on most pages a Previous card too — moves to neighboring pages. Setup guides also show a sentence above them — If you want to continue setup, read — with a link to the next configuration step, so you can wire providers in order. ## The copy tools Two buttons sit under every guide's title. Copy as Markdown puts the whole guide on your clipboard as Markdown — the same plain-text format the .md version of the page serves — including its commands, lists and the link to the next guide. Copy prompt copies a single ready-made line: Read followed by the address of this page's Markdown version. Paste it into an AI chat and the model fetches the guide itself. Next to them, Ask about this guide opens ChatGPT in a new tab with that same instruction already filled in. The Open in menu offers the same ChatGPT shortcut plus Markdown, which opens the plain-text version in your browser — the same page address with .md at the end. Every command block carries a small Copy button in its top corner. It copies only the commands inside that block, not the explanation around them, so you can paste straight into a terminal. Each button briefly shows Copied once it has worked. ## Give a guide to a coding agent A coding agent — an AI assistant that can read and edit your project, such as Claude Code, Cursor or Codex — works better when it reads the same instructions you do. The simplest hand-off is Copy prompt: paste the copied line into the agent's chat and it reads the guide's Markdown version itself. Choose Copy as Markdown when you want the full text inline — for example so the agent can compare the guide with your files, or so a chat without web access still sees it. You can also paste the page address ending in .md into almost any AI chat; it returns the same Markdown without the page decoration. The kit was written for this workflow — inside your repository, AGENTS.md and the prompts/ folder give an agent its rules and ready-made tasks. [How the kit is built for coding agents](https://supastack.dev/docs/web/agents.md) [Let an agent run the setup for you](https://supastack.dev/docs/web/agent-setup.md) ## The same content inside your repository You do not need this website for every answer — much of it ships inside the code you downloaded. In the Web repository and the All Platforms monorepo, the docs/ folder holds longer Markdown references, such as agent-setup.md (the unattended setup runbook) and upgrading.md (the detailed version of the update walkthrough in this group), and the prompts/ folder holds paste-ready instructions. Standalone Mobile and Desktop checkouts carry a smaller curated set instead. Either way, the llms.txt file at the repository root is a map: it lists which files an agent should read first and the rules it must follow. Your running app serves the same idea itself: it has its own /docs guides for the product you build, and a /llms.txt route that hands an agent a live index of the app. ## Link to a section, not just a page Every section heading on a guide page is also an anchor — a linkable position inside the page. Select a section in the On this page column, or select the heading itself, and your browser's address bar gains a #section-name ending. That full address opens the page already scrolled to the section. Share an anchored address with a teammate when you mean one specific step, or paste it into an agent prompt for the same reason. Two companion pages live in this group: the glossary below, and the update walkthrough for when the kit itself improves. [Words you will see — the glossary](https://supastack.dev/docs/web/glossary.md) [Update your copy when the kit improves](https://supastack.dev/docs/web/updating-the-kit.md) --- Source: [Words you will see](https://supastack.dev/docs/web/glossary.md) # Words you will see Plain-language definitions for the terms the setup guides, error messages and the product itself use. ## Files and folders These words describe the code you downloaded. The setup guides name them whenever they ask you to open or edit a file. - Repository — the project folder plus its full change history, hosted on GitHub and copied to your computer. - Clone — your own copy of a repository on your computer; the git clone command creates it. - Monorepo — one repository holding several apps and shared packages; the All Platforms bundle is one. - package.json — the file in every app or package that lists its name, its commands and the libraries it needs. - pnpm-workspace.yaml — the file at the monorepo root listing which folders belong to the workspace. - Lockfile — the file recording the exact version of every installed library, so each install matches. ## Tools on your computer Programs you install once before the first run. The guides name them in commands such as node --version. - Terminal — the window where you type commands, such as PowerShell on Windows or Terminal on macOS. - Node.js — the program that runs JavaScript outside a browser; the kit's server and scripts run on it. - npm — the library installer bundled with Node.js; in this kit you mainly use it to reach Corepack. - Corepack — the tool that switches on the exact package-manager version the project pins. - pnpm — the package manager that downloads libraries and runs project scripts; reach it as corepack pnpm. - Git — the tool that downloads your repository and records every change you make to it. - Editor or IDE — the app where you open folders and edit files; the guides use Visual Studio Code. ## Configuration words Settings live in files, not in the terminal. These words cover where they sit and what they are called. - Environment variable — a named setting such as EMAIL_PROVIDER=local that changes how the app behaves. - .env — the file at the repository root holding settings for commands such as database migrations. - .env.local — the settings file for one app, such as apps/web/.env.local for the web server. - Secret — a value that must stay private, such as a password or signing key; never commit it or paste it into chats. - API key — a secret string an outside service gives you so it recognizes your app as an allowed caller. - Provider — an outside service the kit can use for AI, email, billing, storage and similar jobs. ## Words the product uses The running app and its settings screens use these terms. Workspace appears with two different meanings — check the context. - Workspace — in the product, one team's separate users, data and settings; in pnpm commands, one package folder inside the monorepo, selected with --filter. - Entitlement — the server's record that a workspace is allowed to use a paid feature. - Migration — a versioned database change that creates or updates tables; you apply migrations with a command. - Session — the signed-in state that tells the server who is asking, until it expires or you sign out. - Webhook — a message an outside service sends to your app's URL when something happens, such as a paid invoice. - Account vs organization — an account is one person's sign-in; an organization is the shared team space members join, also called a workspace. ## Web development words Terms the run, build and deploy guides use when you open the app in a browser. Ctrl + C means press and hold Ctrl, tap C, then release — it stops the running command in a terminal. [Learn commands, files and configuration hands-on](https://supastack.dev/docs/web/terminal-basics.md) [How to use these docs](https://supastack.dev/docs/web/using-the-docs.md) - localhost — the name for this computer in a browser address; your app runs there during development. - Port — the numbered door after localhost, such as :3000; each running app uses a different one. - Dev server — the command that keeps rebuilding and serving the app while you work; stop it with Ctrl + C. - Build — the step that turns your source code into the optimized files a server deploys. - Server rendering (SSR) — pages the server assembles before sending them, so the first view arrives complete. --- Source: [Update your copy when the kit improves](https://supastack.dev/docs/web/updating-the-kit.md) # Update your copy when the kit improves Bring improvements from the source repository into your copy — record your starting point, fetch the new revision, merge it on a branch and verify before deploying. ## 1. Record what you started from Treat an update as a small project, not a paste: you compare the kit revision you have with the newer one, carry the changes over and test in a branch before deploying. This works whether your project is a fork of a kit repository — a copy GitHub keeps connected to the source — or code you copied into your own app. Before anything else, write a note in your project with three things: the kit commit you started from — the SHA, Git's short code that identifies one exact version — the repository you received (supastack-web, supastack-mobile, supastack-desktop or the All Platforms monorepo), and the changes you have already made yourself. The kit repositories and the monorepo have different histories, so never merge a branch from one into a fork of another. If you never recorded a starting SHA, find the closest shared version with git merge-base in a fork, or identify the last files and tests you copied in a selective adoption — and review a wider diff when the exact baseline is uncertain. ## 2. Fetch and merge the new revision Run the commands below in a terminal inside your project folder, with your work saved and committed — a clean working tree, so nothing half-done mixes into the merge. Words inside angle brackets are replace-me templates: substitute your own values in place of the whole . The GitHub address shown is a placeholder; use the address of the repository you forked. What the lines do: git remote add teaches your copy a second address named kit-upstream — the kit repository — and git fetch downloads its newest state without touching your files. git switch -c creates a separate upgrade branch so your main branch stays untouched while you review. git log lists the commits you are about to take and git diff --stat summarizes which files changed; read the full diff for the modules you use, not only the commit titles. git merge brings the new code into the upgrade branch. If Git reports the kit-upstream remote already exists, skip the remote add line and continue with git fetch. is the starting SHA you recorded in step 1. ``` # Upstream is the platform repository you forked — substitute its real URL # (supastack-web shown; use supastack-mobile or supastack-desktop as fits). git remote add kit-upstream https://github.com//supastack-web.git git fetch kit-upstream git switch -c upgrade/kit-update git log --oneline ..kit-upstream/main git diff --stat ..kit-upstream/main git merge kit-upstream/main ``` ## 3. Resolve conflicts without losing your work A conflict means both sides changed the same lines: the kit moved on and you customized the same place. Git pauses the merge and marks the file — it is asking you to choose, not reporting damage. Open each marked file in your editor and decide line by line: keep your product's own text, names and settings, and take the kit's fixes and structural changes. Never resolve a conflict by replacing a file you customized with the kit's whole new version — that erases your work. Resolve inside the upgrade branch and run the tests for each resolved area before continuing. If the merge goes wrong, git merge --abort returns the branch to its pre-merge state. Nothing in this flow changes your main branch or your deployment until you merge the upgrade branch yourself. ## 4. Update data and deploy in the right order New kit revisions can change the database. Read the new files in packages/db/drizzle/ — the migration folder — and back up production data before applying migrations; test them against a disposable copy first. Deploy compatible schema changes before the code that needs them: if a migration removes or reshapes data, rolling back app code alone cannot restore the old schema. Compare the new .env.example with your own settings and add newly required values in your hosting dashboard. .env files carry secrets and are never committed to Git — you recreate them by hand on each machine and deployment, and they stay out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. Before deploying, run the kit's own verification gates. In the All Platforms monorepo, corepack pnpm check:local runs lint, typecheck, the unit tests and a format check with no paid credentials; corepack pnpm verify:local adds the production build and the browser test suites. In a single-surface fork, run that repository's own test and build commands. Then do a staging smoke pass: sign in, try a second workspace, and run your product's main user journey. When everything passes, write down the accepted target SHA and the conflicts you resolved — that note is the starting baseline for the next update. ``` corepack pnpm check:local # fuller gate: adds the production build and browser suites corepack pnpm verify:local ``` ## 5. If you copied pieces instead of forking A selective adoption — copying kit modules into an existing app — shares no Git history with the kit, so git merge cannot help. Compare the old source SHA with the target source SHA instead, and port only the changes that touch your adopted slice. Review the module's public types, persistence schema and migrations, server caller, configuration, tests and docs as one unit — a change inside a package can require a matching route or environment change elsewhere, and copying a single helper can leave the rest on an outdated contract. Keep a short adaptation note — which kit files you took, which you skipped, what you changed — so the next update can replay your choices. If an update breaks something in your running copy, work through the troubleshooting guide before undoing your own changes. [Fix a problem after an update](https://supastack.dev/docs/web/troubleshooting.md) --- Source: [Ship mobile in 5 minutes](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) # Ship mobile in 5 minutes Open the Expo app on a phone or simulator, configure an API address the device can reach, and distinguish a UI preview from real sign-in and native purchases. ## 1. Prepare the computer, phone and backend Complete Before you start and open the Mobile repository or All Platforms monorepo in your editor. In Terminal → New Terminal at the repository root, run the install command below. Wait for it to finish. Install Expo Go on your phone using the official Expo page linked below; its version must support the Expo SDK version recorded in apps/mobile/package.json. A backend is the Web server that handles sign-in, workspace data and AI requests. If your checkout contains apps/web, first run the Web setup in another terminal and leave that server running. Complete its Postgres setup before trying native sign-up or sign-in. A Mobile-only checkout does not contain apps/web: connect it to your running, separately provided backend instead. Starting Expo starts the screen-preview server, not the backend. Put your phone and computer on the same Wi-Fi. Temporarily disconnect a VPN if it blocks local connections. A phone's localhost means the phone itself. Use your computer's private network address for a physical phone or Android device; iOS Simulator on the same Mac can use http://localhost:3000, and Android Emulator commonly uses http://10.0.2.2:3000. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) [Start the Web backend](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Install an Expo Go version compatible with your project](https://expo.dev/go) ``` corepack pnpm install ``` ## 2. Find and test the backend address On Windows run ipconfig in a new PowerShell terminal, find your active Wi-Fi adapter and read IPv4 Address. On macOS open System Settings → Wi-Fi → Details → TCP/IP and read IP address. On Linux open your network settings and read IPv4 Address. An address might look like 192.168.1.25; yours will usually be different. If your computer's address is 192.168.1.25, open http://192.168.1.25:3000 in the phone's browser while the Web server is running. Substitute your actual address. The homepage must load before you proceed. Allow the Node app on private networks if Windows Firewall asks. If it does not load, check the address, Wi-Fi, firewall and running Web server; an Expo tunnel only carries the preview bundle and does not make your separate API reachable. ``` # Windows PowerShell only: ipconfig ``` ## 3. Create the Mobile app's configuration file In the editor's Explorer, expand apps → mobile, create a new file named .env.local, and paste the configuration below into that file. Replace 192.168.1.25 with the address you just tested, or use your provided HTTPS backend address. Save the file. Its full path is apps/mobile/.env.local; root .env is not automatically read by Expo. Keep the phone's purchase setting off for the first preview. EXPO_PUBLIC_ means a setting is included in the phone app, so these values must contain only public URLs, schemes and public client settings. Do not paste AI-provider, database or billing secrets here. The Web server has its own apps/web/.env.local; changing the Mobile file does not change the server. ``` EXPO_PUBLIC_API_URL=http://192.168.1.25:3000 EXPO_PUBLIC_APP_SCHEME=supastack EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=off ``` ## 4. Start Expo and open the preview From the repository root, run the command below. Wait for a QR code and the Metro waiting message. Keep this terminal and the backend terminal open. On Android open Expo Go and scan the QR code; on iPhone use Camera, scan the code and open it in Expo Go. The QR code opens the development app, not your backend homepage. Success means the app's sign-in screen opens on your device. With purchases off, this is a UI preview; complete the database-backed Web authentication setup before testing sign-in, saved workspaces or chat. If an installed simulator is your target, press a in the Expo terminal for Android or i for iOS. iOS Simulator requires macOS and Xcode; it cannot run on Windows. ``` corepack pnpm --filter @supastack/mobile start ``` ## 5. Recover from preview errors and test real flows If Expo Go reports an incompatible project, compare its supported SDK with apps/mobile/package.json and use the compatible version from the official Expo page. Do not upgrade the kit's dependency versions just to dismiss the message. If a required native module is unavailable, follow Mobile builds for a development build containing that module. If sign-in fails with a network error, retry the backend URL in the phone's browser first. If the URL works but sign-in fails, inspect the backend terminal and verify Postgres and migrations. The local /dashboard demo session does not sign you in on the phone. If configuration seems stale, press Ctrl + C in the Expo terminal, then run the restart command below and reopen the preview. Expo Go cannot prove real StoreKit or Play Billing purchase/restore behavior. Follow In-app purchases and Mobile builds for an EAS development build and store test accounts. First-time native builds and account enrollment can take longer than five minutes. To stop this preview, press Ctrl + C in the Expo terminal; stop the backend separately when finished. [Set up and test native sign-in](https://supastack.dev/docs/mobile/authentication.md) [Create a native development build](https://supastack.dev/docs/mobile/builds.md) [Configure native purchase and restore](https://supastack.dev/docs/mobile/revenuecat.md) ``` corepack pnpm --filter @supastack/mobile start --clear ``` --- Source: [Mobile overview](https://supastack.dev/docs/mobile.md) # Mobile overview An Expo surface connected to the same server-owned foundation. ## Your first Mobile session Follow the Mobile tutorial below from a computer, with a phone or installed simulator nearby. You will install the project, write a public API address in apps/mobile/.env.local and scan an Expo QR code. Expo is the development tool that opens your React Native screens on a device. The Next.js API and shared server packages remain the source for accounts, data and permissions: this is the running Web backend your phone talks to. Expo's QR server only delivers the screen code. A successful preview does not mean the backend, sign-in or store purchases have been configured; the tutorial shows how to test each boundary in order. [Follow the complete Mobile first-run tutorial](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) ## Share contracts, keep native presentation Expo includes native sign-in and session handling, workspace and settings surfaces, chat controls, and RevenueCat purchase workflows. The Next.js API and shared server packages remain the source for shared data, authorization, and provider verification; native screens call them instead of embedding server credentials. The Expo application lives in apps/mobile. It consumes the shared foundation through an API rather than embedding server credentials. Native authentication, secure session storage, deep links, chat controls, and RevenueCat purchase presentation each have client modules. ## Configure the API and app scheme Set EXPO_PUBLIC_API_URL to an API origin reachable by your emulator or physical device. Localhost on a phone refers to that phone, so use the appropriate reachable development host. EXPO_PUBLIC_APP_SCHEME identifies the app’s deep-link scheme. Review apps/mobile/app.config.ts and the authentication configuration before testing callbacks. Public Expo values are bundled into the client and must contain no provider secrets. ## Verify on the target device Run deterministic session-storage, deep-link, chat-cancellation, and entitlement tests first. Then verify sign-in return, session restoration, sign-out, and purchase sync in the intended EAS development build; Expo Go cannot validate real store purchases. Physical-device and provider acceptance are separate from the local web golden path. Track them explicitly for each release target. --- Source: [Run the mobile app](https://supastack.dev/docs/mobile/local-setup.md) # Run the mobile app Connect Expo to a reachable development API. ## 1. Prepare the computer, phone and backend Complete Before you start and open the Mobile repository or All Platforms monorepo in your editor. In Terminal → New Terminal at the repository root, run the install command below. Wait for it to finish. Install Expo Go on your phone using the official Expo page linked below; its version must support the Expo SDK version recorded in apps/mobile/package.json. A backend is the Web server that handles sign-in, workspace data and AI requests. If your checkout contains apps/web, first run the Web setup in another terminal and leave that server running. Complete its Postgres setup before trying native sign-up or sign-in. A Mobile-only checkout does not contain apps/web: connect it to your running, separately provided backend instead. Starting Expo starts the screen-preview server, not the backend. Put your phone and computer on the same Wi-Fi. Temporarily disconnect a VPN if it blocks local connections. A phone's localhost means the phone itself. Use your computer's private network address for a physical phone or Android device; iOS Simulator on the same Mac can use http://localhost:3000, and Android Emulator commonly uses http://10.0.2.2:3000. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) [Start the Web backend](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Install an Expo Go version compatible with your project](https://expo.dev/go) ``` corepack pnpm install ``` ## 2. Find and test the backend address On Windows run ipconfig in a new PowerShell terminal, find your active Wi-Fi adapter and read IPv4 Address. On macOS open System Settings → Wi-Fi → Details → TCP/IP and read IP address. On Linux open your network settings and read IPv4 Address. An address might look like 192.168.1.25; yours will usually be different. If your computer's address is 192.168.1.25, open http://192.168.1.25:3000 in the phone's browser while the Web server is running. Substitute your actual address. The homepage must load before you proceed. Allow the Node app on private networks if Windows Firewall asks. If it does not load, check the address, Wi-Fi, firewall and running Web server; an Expo tunnel only carries the preview bundle and does not make your separate API reachable. ``` # Windows PowerShell only: ipconfig ``` ## 3. Create the Mobile app's configuration file In the editor's Explorer, expand apps → mobile, create a new file named .env.local, and paste the configuration below into that file. Replace 192.168.1.25 with the address you just tested, or use your provided HTTPS backend address. Save the file. Its full path is apps/mobile/.env.local; root .env is not automatically read by Expo. Keep the phone's purchase setting off for the first preview. EXPO_PUBLIC_ means a setting is included in the phone app, so these values must contain only public URLs, schemes and public client settings. Do not paste AI-provider, database or billing secrets here. The Web server has its own apps/web/.env.local; changing the Mobile file does not change the server. ``` EXPO_PUBLIC_API_URL=http://192.168.1.25:3000 EXPO_PUBLIC_APP_SCHEME=supastack EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=off ``` ## 4. Start Expo and open the preview From the repository root, run the command below. Wait for a QR code and the Metro waiting message. Keep this terminal and the backend terminal open. On Android open Expo Go and scan the QR code; on iPhone use Camera, scan the code and open it in Expo Go. The QR code opens the development app, not your backend homepage. Success means the app's sign-in screen opens on your device. With purchases off, this is a UI preview; complete the database-backed Web authentication setup before testing sign-in, saved workspaces or chat. If an installed simulator is your target, press a in the Expo terminal for Android or i for iOS. iOS Simulator requires macOS and Xcode; it cannot run on Windows. ``` corepack pnpm --filter @supastack/mobile start ``` ## 5. Recover from preview errors and test real flows If Expo Go reports an incompatible project, compare its supported SDK with apps/mobile/package.json and use the compatible version from the official Expo page. Do not upgrade the kit's dependency versions just to dismiss the message. If a required native module is unavailable, follow Mobile builds for a development build containing that module. If sign-in fails with a network error, retry the backend URL in the phone's browser first. If the URL works but sign-in fails, inspect the backend terminal and verify Postgres and migrations. The local /dashboard demo session does not sign you in on the phone. If configuration seems stale, press Ctrl + C in the Expo terminal, then run the restart command below and reopen the preview. Expo Go cannot prove real StoreKit or Play Billing purchase/restore behavior. Follow In-app purchases and Mobile builds for an EAS development build and store test accounts. First-time native builds and account enrollment can take longer than five minutes. To stop this preview, press Ctrl + C in the Expo terminal; stop the backend separately when finished. [Set up and test native sign-in](https://supastack.dev/docs/mobile/authentication.md) [Create a native development build](https://supastack.dev/docs/mobile/builds.md) [Configure native purchase and restore](https://supastack.dev/docs/mobile/revenuecat.md) ``` corepack pnpm --filter @supastack/mobile start --clear ``` --- Source: [Native authentication](https://supastack.dev/docs/mobile/authentication.md) # Native authentication Handle the browser return path and stored sessions on mobile. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/mobile/src/auth/client.ts and apps/mobile/src/auth/mobile-auth-configuration.ts in VS Code to find the existing sign-in flow. 2. Put EXPO_PUBLIC_API_URL and EXPO_PUBLIC_APP_SCHEME in apps/mobile/.env.local, and make sure the Web server has its own settings in apps/web/.env.local. 3. From the root start Web with corepack pnpm --filter @supastack/web dev, then in another terminal start Expo with corepack pnpm --filter @supastack/mobile start. Expected: a test sign-in opens the browser and returns to the mobile app. On a physical phone, use the computer's reachable Wi-Fi/LAN address for the API; phone-localhost means the phone itself. The mobile app uses the Better Auth Expo integration and native secure storage. API origin and app scheme must agree with the native configuration and the authentication server's allowed return flow. ``` EXPO_PUBLIC_API_URL=https://your-api.example EXPO_PUBLIC_APP_SCHEME=supastack ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Terminal 1 — Web API (from the monorepo root): corepack pnpm --filter @supastack/web dev # Terminal 2 — Expo app (from the monorepo root): corepack pnpm --filter @supastack/mobile start ``` ## Implementation workflow Follow the existing native authentication client instead of copying web cookie assumptions into mobile. Open provider authentication through the configured browser flow, handle its callback, then resolve the authenticated session before presenting protected actions. Keep cancelled sign-in distinct from successful sign-in. Members can enable biometric app lock (Face ID, Touch ID, or fingerprint in `apps/mobile/src/security/app-lock.ts` under Settings → Security) to gate foreground resume. Clear the previous user's local presentation and workspace-scoped offline read cache (`apps/mobile/src/offline/`) on sign-out, and verify the server session can no longer perform protected work. ## Verification and troubleshooting If sign-in opens a browser but does not return to the app, check that EXPO_PUBLIC_APP_SCHEME matches the scheme in apps/mobile/app.config.ts. If the app cannot reach the server, replace localhost with the computer's LAN address, confirm both devices use the same network, then restart Expo after editing apps/mobile/.env.local. Expo Go is a UI preview; native purchase acceptance needs an EAS development build. Exercise cold-start callbacks, returning to a running app, cancelled sign-in, and expired sessions on the intended build. If the browser returns but the app remains signed out, inspect scheme matching and API reachability before changing provider configuration. --- Source: [Mobile API](https://supastack.dev/docs/mobile/api.md) # Mobile API How the app talks to the server — sessions, reachability, and payloads. ## Beginner walkthrough Prerequisites: Read the two beginner guides below and finish Mobile local setup. Put Expo values in `apps/mobile/.env.local`; set `EXPO_PUBLIC_API_URL` to a backend host your device can reach. A mobile-only checkout has no web backend, so start or connect one separately. A successful Expo export proves the JavaScript bundle, not device or purchase acceptance. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. In `apps/mobile/.env.local`, set `EXPO_PUBLIC_API_URL` to the running backend address reachable from your phone; do not use `localhost` from a physical device. 2. Follow Mobile local setup to start Expo, sign in, and open workspace, settings, and chat screens. 3. Expect authenticated data from the same workspace. If requests time out, check the phone and computer network and start the backend separately in a mobile-only checkout. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Mobile local setup](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Understand native scope Expo includes native auth/session and deep links alongside workspace, settings, chat, and RevenueCat purchase surfaces. Shared business behavior and authorization stay in the web backend/packages; native clients under `apps/mobile/src` call those APIs without embedding provider credentials. ## Use the session-authenticated BFF The `/api/mobile` routes cover entitlement, RevenueCat purchase availability and server sync, organization, members, invitations, audit, API keys, webhooks, domains, schedules and feature requests. They preserve workspace ownership and role checks. They are different from the `sk_` bearer-key `/api/v1` public contract. Use your device-reachable `EXPO_PUBLIC_API_URL` and the configured origin/scheme; public Expo variables never contain provider credentials. ## Verify on native targets Check loading/error/empty states, reconnect, expired session, forbidden management actions and deep-link returns on the device/build you intend to support. An Expo export proves bundling, not native delivery or purchase acceptance. [All API routes](https://supastack.dev/docs/web/project-structure.md) [Native session flow](https://supastack.dev/docs/mobile/authentication.md) ## Setup and configuration The app calls the API under /api/mobile and the versioned /api/v1 surface with the member's session rather than cookie assumptions from the web app. EXPO_PUBLIC_API_URL is the only origin the client knows; it is public by design and carries no secrets. Session tokens come from the Better Auth Expo flow and are kept in native secure storage — never in plain AsyncStorage or bundled config. ``` GET /api/mobile/entitlement # access state for the member GET /api/mobile/organization # active workspace context GET /api/mobile/usage # current-month AI spend, meter caps, and credit balance GET /api/mobile/appearance # synced appearance bundle (GET/PUT) POST /api/v1/ai-tasks # authorized work, same as web Session: /api/auth/* (Better Auth Expo flow) # Optional server-only token for authenticated Expo push sends: # EXPO_ACCESS_TOKEN= ``` ## Implementation workflow Resolve the stored session before the first authenticated request, refresh it when the server reports expiry, and sign out cleanly when it is revoked. Treat unreachable API and invalid session as different states — one retries, the other returns to sign-in. Keep payloads identical to the web contract: the server applies the same workspace, entitlement, and validation rules regardless of which client called. ## Verification and troubleshooting Test airplane-mode failures, expired tokens mid-session, revoked sessions, and a cold start with a stored valid token. A request that fails closed (denied) is correct; one that fails open is a bug to fix before release. --- Source: [Native UI and theming](https://supastack.dev/docs/mobile/ui.md) # Native UI and theming Screen primitives and shared view-models that mirror the web's design decisions. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Mobile first-run tutorial](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/mobile start ``` ## Setup and configuration Screen primitives live in apps/mobile/src/ui — Screen, Card, Title, Detail, SectionLabel, ErrorText, AppButton — styled from apps/mobile/src/theme, so a visual decision made for the web maps onto the native chrome. In Appearance settings, the Match web app mode (apps/mobile/src/theme/synced.ts via GET/PUT /api/mobile/appearance) applies the signed-in user's synced web color pack from a local cache. packages/ui-native carries the cross-platform view-model helpers: createEntitlementCard turns server entitlement state into a card's title, status, detail, action label, and accessibility label, so web and mobile render the same access decision. ``` apps/mobile/src/ui/screen.tsx exports: Screen, Card, H1, H2, Title, Detail, SectionLabel, ErrorText, AppButton @supastack/ui-native → createEntitlementCard(input) → { title, status, detail, accessibilityLabel, action } ``` ## Implementation workflow Compose screens from the shared primitives before writing new elements; match the theme module's tokens and density instead of hardcoding sizes and colors. Keep product copy out of primitives — they render labels, not copy decisions. Carry accessibility labels through custom rows the way createEntitlementCard does. ## Verification and troubleshooting Verify a screen in light and dark, at the largest text size, with screen-reader labels, and on a small device. A component that truncates or clips at 200% text is not done. --- Source: [Mobile entitlements](https://supastack.dev/docs/mobile/entitlements.md) # Mobile entitlements Configure RevenueCat purchases and keep access decisions on the server. ## Understand the access status shown on your phone An entitlement is permission to use a paid feature. It belongs to the active workspace, the group of account data your team uses. First complete native sign-in, then open the phone app's Settings → Billing. Read the displayed access status; loading or an error does not mean you have a subscription. With purchases off, the app should not offer store checkout. To sell access, complete the RevenueCat purchase guide below and test in a native development build. After a test purchase, reopen Billing and confirm the server reports active access; after expiry, confirm the feature is locked again. If a purchase succeeds but access does not refresh, use Restore purchases and check the backend connection rather than buying a second time. [Sign in from the native app](https://supastack.dev/docs/mobile/authentication.md) [Configure store products and restore purchases](https://supastack.dev/docs/mobile/revenuecat.md) ## Read access from the server The mobile entitlement client reads server-owned access state. Keep a loading or failed request distinct from an active subscription; the interface must not infer paid access because a request failed. Use the presentation logic in apps/mobile/src/entitlements to translate that state into the native interface. ## Keep the purchase boundary explicit Native purchases use RevenueCat when configured; web billing is a separate surface. Do not expose Stripe Checkout in the mobile interface or treat a store-side active flag as proof of server access. The [In-app purchases guide](https://supastack.dev/docs/mobile/revenuecat.md) documents client keys, manager checks, workspace identity, purchase/restore sync, server verification, and sandbox setup. Real StoreKit or Play Billing acceptance requires an EAS development build; Submit to the app stores covers release. ## Check denied and expired access Test unavailable, inactive, and active entitlement states, plus network failures. Verify that restricted actions remain unavailable when the server denies access, and that signing out clears the previous session’s presentation. --- Source: [Create mobile accounts](https://supastack.dev/docs/mobile/external-accounts.md) # Create mobile accounts Expo, Apple, Google Play, and RevenueCat accounts — what each one is for and when to create it. ## When you need these The daily mobile setup needs none of them: Expo Go plus a running backend — `EXPO_PUBLIC_API_URL` in `apps/mobile/.env.local` — covers previewing screens and testing sign-in. Create these accounts when you move past Expo Go: an Expo account powers EAS cloud builds, the store accounts enable submission, RevenueCat verifies native purchases, and Apple's program is also required for Sign in with Apple during store review. Two of them cost money: Apple Developer is $99 per year, Google Play is a one-time $25 registration; Expo and RevenueCat both have free tiers. [Run the mobile app first](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/web/aws-ses.md) ## Expo + EAS 1. Open [expo.dev](https://expo.dev) → Sign up, create the account, and confirm the email. 2. In a terminal run `npx eas-cli@latest login` and enter the same credentials — `eas` is the command-line tool for Expo Application Services builds and submissions. 3. From the `apps/mobile` folder run `npx eas-cli@latest init` to link the codebase to an EAS project; it writes the project id into the `app.json`/`app.config.ts` configuration. The `eas.json` build profiles are already shipped. Expected: `npx eas-cli@latest whoami` prints your account name and the project appears in your expo.dev dashboard. ``` npx eas-cli@latest login cd apps/mobile npx eas-cli@latest init npx eas-cli@latest whoami ``` ## Apple Developer Program 1. Open [developer.apple.com](https://developer.apple.com) → Account → enroll in the Apple Developer Program — $99 per year; an individual membership is fine. 2. Enrollment requires an Apple ID with two-factor authentication enabled and a legal name matching your payment method; approval can take a day or two. 3. This one account covers two requirements: submitting the iOS app to the App Store, and Sign in with Apple — App Review requires offering it whenever an iOS app ships third-party social login such as Google or GitHub. The kit wires it through `AUTH_APPLE_ENABLED` plus the `APPLE_CLIENT_ID`/`APPLE_CLIENT_SECRET` pair on the backend. The same membership also covers macOS desktop signing — the desktop account guide explains that side. ## Google Play Console 1. Open [play.google.com/console](https://play.google.com/console), pay the one-time $25 registration, and create the developer account in your name or your company's. 2. Stop at the developer account for now — the app entry itself gets created inside Play Console during submission: the first upload of a new app always happens manually there, and `eas submit` takes over from the second release. ## RevenueCat — only if the app sells content Skip this section entirely if the mobile app does not sell digital content or subscriptions. If it does: 1. open [revenuecat.com](https://www.revenuecat.com) and create a free account, 2. create a project, 3. register the iOS app (connects to App Store Connect) and the Android app (Play package name plus service credentials) inside it. 4. Create the products and an entitlement matching your store products — the kit checks the configured entitlement (`REVENUECAT_ENTITLEMENT_ID`, `REVENUECAT_ENTITLEMENT_KEY`) server-side before granting access. 5. Copy each platform's public SDK key into `apps/mobile/.env.local` and enable the provider on both sides, as below. The SDK keys are public client values; `REVENUECAT_SECRET_API_KEY` and `REVENUECAT_WEBHOOK_AUTHORIZATION` are server-only and belong in the backend's environment — never in an `EXPO_PUBLIC_` variable. [Set up RevenueCat purchases step by step](https://supastack.dev/docs/mobile/revenuecat.md) ``` # apps/mobile/.env.local EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=revenuecat EXPO_PUBLIC_REVENUECAT_IOS_API_KEY=your-public-ios-sdk-key EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY=your-public-android-sdk-key # apps/web/.env.local — server side MOBILE_PURCHASES_PROVIDER=revenuecat REVENUECAT_SECRET_API_KEY=your-secret-api-key REVENUECAT_WEBHOOK_AUTHORIZATION=your-webhook-auth-value ``` ## Sign-in providers on mobile Mobile sign-in reuses the same Google and GitHub OAuth apps you create for the web — the callbacks stay `{APP_URL}/api/auth/callback/` because Better Auth handles the server side, and the app returns through its `EXPO_PUBLIC_APP_SCHEME` deep-link scheme. No extra provider app is needed just because the client is native. The exception is iOS store review: shipping Google or GitHub buttons on iOS requires offering Sign in with Apple too, which is why the Apple Developer account above doubles as a sign-in dependency. [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) ## Done means Check each account before moving on: `npx eas-cli@latest whoami` prints your Expo account and the project shows in the dashboard; the Apple Developer membership reads Active; the Play Console account exists; the RevenueCat project lists both store apps with products and an entitlement, and its keys are in the right env files. Then continue in order — connect the project, set up purchases if the app sells content, build for a device, submit: [Connect the project to EAS](https://supastack.dev/docs/mobile/eas-setup.md) [Set up RevenueCat purchases](https://supastack.dev/docs/mobile/revenuecat.md) [Build and verify on devices](https://supastack.dev/docs/mobile/builds.md) [Submit to the app stores](https://supastack.dev/docs/mobile/publishing.md) --- Source: [Set up RevenueCat purchases](https://supastack.dev/docs/mobile/revenuecat.md) # Set up RevenueCat purchases Create a RevenueCat project, link your App Store and Play products, and wire the SDK keys plus server secrets that verify purchases. ## Beginner walkthrough Why this exists: Apple and Google require digital purchases inside an app to go through their own billing — never Stripe Checkout — and the kit enforces that boundary. RevenueCat sits on top of both stores so one integration covers iOS and Android: it tracks receipts, subscriptions, and restores, and your server asks RevenueCat whether a workspace paid. Prerequisites: the Apple Developer Program ($99 per year) and Google Play Console ($25 once) accounts from the mobile accounts guide — store products must exist before RevenueCat can link them — plus an EAS development build to actually test, because Expo Go cannot run the native purchase module at all. RevenueCat's free tier covers testing; none of this charges you before products are live. Quick terms: a RevenueCat project holds your store-linked apps. Products are the store items mirrored inside RevenueCat. An entitlement is the flag RevenueCat grants while a subscription is active — `pro` by default. An offering bundles products into the paywall the app presents. The public SDK keys go in the mobile env; a secret API key and webhook authorization stay server-side. What you create: one project, two store-linked apps, products, one entitlement, one offering — about an hour, mostly waiting on store product setup. [Create the mobile accounts](https://supastack.dev/docs/mobile/external-accounts.md) [Build a development client](https://supastack.dev/docs/mobile/eas-setup.md) ## Create the RevenueCat account and project 1. Open [app.revenuecat.com](https://app.revenuecat.com) → sign up and create a project with your product's name. Expected: an empty project dashboard with tabs for Apps, Products, Entitlements, Offerings, and API keys — the next sections fill them in order. ## Create the store products, then link both apps Store-side products must exist before RevenueCat can reference them. iOS: [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → My Apps → create the app record with your `ios.bundleIdentifier` → In-App Purchases or Subscriptions → create the subscription products you plan to sell (they may sit in draft while you test). Android: [play.google.com/console](https://play.google.com/console) → create the app → Monetize → Products → Subscriptions (or in-app products). Then link both inside RevenueCat: Apps → New app. For iOS pick App Store, enter the bundle id, and supply the App Store Connect credentials the form asks for — it links RevenueCat's own setup docs for each field. For Android pick Play Store, enter the package name, and upload the Play service-account JSON — the linked docs walk creating it in Google Cloud. Expected: both apps listed under the project, and the store consoles show your products existing — draft state is fine at this stage. ## Products, entitlement, offering 1. Products → New: create one RevenueCat product per store product and attach the matching App Store or Play product id — the ids must match the consoles exactly. 2. Entitlements → New: name it to match `REVENUECAT_ENTITLEMENT_ID` — the kit's default is `pro` — and attach the products that grant it. 3. Offerings → New: create an offering, add packages pointing at your products, and mark it Current — the app reads `offerings.current` when rendering the paywall. Keep `REVENUECAT_ENTITLEMENT_KEY` at its default `ai.reference-task`: that is the kit's internal ledger key — the same key the shipped web plans grant — and RevenueCat access is written under it. Rename it only when your product's plan key changes. ## Copy the keys and register the webhook Project → API keys lists the three kinds below. The public per-platform SDK keys (`appl_…` for iOS, `goog_…` for Android) go in `apps/mobile/.env.local` — they identify the app, carry no spend authority, and the `EXPO_PUBLIC_` prefix is correct for them. The secret API key (`sk_…`, the v1 key) and the webhook authorization are server-only in `apps/web/.env.local` — never `EXPO_PUBLIC_`, never committed. The server calls RevenueCat's subscriber API with the secret key to verify each workspace's purchase state. Webhook: RevenueCat → Integrations → Webhooks → set the URL `{APP_URL}/api/billing/webhook/revenuecat` and an Authorization header value — any long random string you invent — then put the exact same string into `REVENUECAT_WEBHOOK_AUTHORIZATION`. The kit compares the header verbatim and rejects anything else, so renewals, cancellations, and refunds reach the server without waiting for the next app launch. Sandbox purchases are accepted automatically outside production; `REVENUECAT_ALLOW_SANDBOX=true` in a production env is rejected at startup, so leave it unset once live. ``` # apps/mobile/.env.local EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=revenuecat EXPO_PUBLIC_REVENUECAT_IOS_API_KEY=appl_… EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY=goog_… # apps/web/.env.local — server side MOBILE_PURCHASES_PROVIDER=revenuecat REVENUECAT_SECRET_API_KEY=sk_… REVENUECAT_WEBHOOK_AUTHORIZATION= REVENUECAT_ENTITLEMENT_ID=pro REVENUECAT_ENTITLEMENT_KEY=ai.reference-task ``` ## Verify with a sandbox purchase Purchases run only inside a development or production build — Expo Go lacks the native module entirely, so this step assumes `eas build --profile development` produced a dev client (the EAS guide builds it). 1. Install the dev build on a device or simulator signed into a sandbox account: App Store Connect → Users and Access → Sandbox Testers for iOS; Play Console → license testing for Android. 2. In the app open the paywall and buy with the sandbox account. Expected: the purchase completes in sandbox — no real charge — Restore purchases works, and the workspace's entitlement shows active, recorded server-side under the `ai.reference-task` key. Common failures: an empty paywall means the offering is not Current or products are unlinked; a failing device purchase means the store product is still in draft or the wrong sandbox account is signed in; an entitlement that never flips means `MOBILE_PURCHASES_PROVIDER`/`EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER` are off or the webhook Authorization mismatches — RevenueCat's dashboard lists each webhook delivery's status. [Build and verify on devices](https://supastack.dev/docs/mobile/builds.md) [Web checkout uses Stripe instead](https://supastack.dev/docs/web/stripe-payments.md) --- Source: [EAS setup](https://supastack.dev/docs/mobile/eas-setup.md) # EAS setup Connect the project to Expo Application Services and produce your first build. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/mobile/eas.json and apps/mobile/app.json in VS Code to review development, preview, and production settings. 2. Add the native development client from the repository root with corepack pnpm --filter @supastack/mobile exec expo install expo-dev-client. Run corepack pnpm --filter @supastack/mobile typecheck, then corepack pnpm --filter @supastack/mobile build for the local JavaScript export. 3. For native testing, sign in to your Expo account, change the terminal folder with cd apps/mobile, then run npx eas-cli@latest build --platform android --profile development. Use ios to request an EAS cloud iOS build from any operating system; a Mac with Xcode is needed to run the iOS Simulator or compile locally. Expected: the export verifies the JavaScript bundle and EAS produces an installable native development app. See [Expo's development build guide](https://docs.expo.dev/develop/development-builds/faq/) for why Expo Go cannot add native libraries. The mobile workspace build invokes its export script. This validates bundling for the configured targets; store signing, distribution configuration, and physical-device acceptance remain separate work. ``` corepack pnpm --filter @supastack/mobile typecheck corepack pnpm --filter @supastack/mobile test corepack pnpm --filter @supastack/mobile build ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # From the monorepo root, install the native development client once: corepack pnpm --filter @supastack/mobile exec expo install expo-dev-client corepack pnpm --filter @supastack/mobile typecheck corepack pnpm --filter @supastack/mobile build # EAS needs to run where apps/mobile/eas.json is located: cd apps/mobile npx eas-cli@latest build --platform android --profile development ``` ## Implementation workflow Run shared contract tests after changing a package consumed by mobile. Review native safe areas, keyboard behavior, readable text sizing, and reduced motion on the devices you intend to support. Keep native purchases behind the existing RevenueCat boundary. A successful bundle must not introduce Stripe Checkout into the mobile interface as a substitute for a verified native purchase integration. Use an EAS development build on iOS or Android to validate `react-native-purchases` against configured sandbox products. Expo Go and the workspace export script do not validate store transactions. ## Verification and troubleshooting If the EAS build says expo-dev-client is missing, repeat its install command and confirm apps/mobile/package.json now lists it. Run EAS from apps/mobile so it finds eas.json. If EAS reports a missing project or signing setup, check your Expo account and project settings. Expo Go and a successful export still do not test StoreKit or Play Billing; use an EAS development build with sandbox products. A cloud iOS build can be started on Windows or Linux; only local iOS Simulator/build work needs a Mac with Xcode. Record the device, build, API environment, and sign-in method used for acceptance. Verify deep-link return, session restoration, denied entitlements, and sign-out. Report export success separately from store and device verification. When the export passes and a release is the goal, follow Submit to the app stores — the shipped eas.json profiles and build:/submit: scripts carry the app to Google Play and the App Store. --- Source: [Build and device verification](https://supastack.dev/docs/mobile/builds.md) # Build and device verification Separate a successful Expo export from release readiness. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/mobile/eas.json and apps/mobile/app.json in VS Code to review development, preview, and production settings. 2. Add the native development client from the repository root with corepack pnpm --filter @supastack/mobile exec expo install expo-dev-client. Run corepack pnpm --filter @supastack/mobile typecheck, then corepack pnpm --filter @supastack/mobile build for the local JavaScript export. 3. For native testing, sign in to your Expo account, change the terminal folder with cd apps/mobile, then run npx eas-cli@latest build --platform android --profile development. Use ios to request an EAS cloud iOS build from any operating system; a Mac with Xcode is needed to run the iOS Simulator or compile locally. Expected: the export verifies the JavaScript bundle and EAS produces an installable native development app. See [Expo's development build guide](https://docs.expo.dev/develop/development-builds/faq/) for why Expo Go cannot add native libraries. The mobile workspace build invokes its export script. This validates bundling for the configured targets; store signing, distribution configuration, and physical-device acceptance remain separate work. ``` corepack pnpm --filter @supastack/mobile typecheck corepack pnpm --filter @supastack/mobile test corepack pnpm --filter @supastack/mobile build ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # From the monorepo root, install the native development client once: corepack pnpm --filter @supastack/mobile exec expo install expo-dev-client corepack pnpm --filter @supastack/mobile typecheck corepack pnpm --filter @supastack/mobile build # EAS needs to run where apps/mobile/eas.json is located: cd apps/mobile npx eas-cli@latest build --platform android --profile development ``` ## Implementation workflow Run shared contract tests after changing a package consumed by mobile. Review native safe areas, keyboard behavior, readable text sizing, and reduced motion on the devices you intend to support. Keep native purchases behind the existing RevenueCat boundary. A successful bundle must not introduce Stripe Checkout into the mobile interface as a substitute for a verified native purchase integration. Use an EAS development build on iOS or Android to validate `react-native-purchases` against configured sandbox products. Expo Go and the workspace export script do not validate store transactions. ## Verification and troubleshooting If the EAS build says expo-dev-client is missing, repeat its install command and confirm apps/mobile/package.json now lists it. Run EAS from apps/mobile so it finds eas.json. If EAS reports a missing project or signing setup, check your Expo account and project settings. Expo Go and a successful export still do not test StoreKit or Play Billing; use an EAS development build with sandbox products. A cloud iOS build can be started on Windows or Linux; only local iOS Simulator/build work needs a Mac with Xcode. Record the device, build, API environment, and sign-in method used for acceptance. Verify deep-link return, session restoration, denied entitlements, and sign-out. Report export success separately from store and device verification. When the export passes and a release is the goal, follow Submit to the app stores — the shipped eas.json profiles and build:/submit: scripts carry the app to Google Play and the App Store. --- Source: [Store publishing](https://supastack.dev/docs/mobile/publishing.md) # Store publishing Separate a working build from a release users can install. ## Prepare your first store submission step by step First create and test a native development build using Mobile builds. A store submission sends the compiled app and its listing to Google or Apple for review; it is not the same as opening an Expo Go preview. You need your own Expo and relevant store developer accounts, a tested HTTPS backend, final app name/icon, screenshots, support contact and a public privacy-policy URL. Open apps/mobile/app.json in the editor and set the app name, slug, ios.bundleIdentifier and android.package for your own product before the first upload. Open apps/mobile/eas.json and replace the iOS submission team's app identifiers with your own where required. The shipped identifiers are examples. Read Mobile builds for the exact fields and ask your coding agent to review the two files before building. If eas is not installed, run npm install --global eas-cli. From the repository root use the login command below, finish browser authentication, then select the build command for one platform in Build, then submit. Read the build's dashboard logs; wait for a successful .aab or .ipa before attempting submission. EAS prompts for project linking and signing credentials; these belong to your accounts, not the kit seller's. After the binary is uploaded, open your app in Play Console or App Store Connect, complete the listing and required questionnaires, add working reviewer sign-in instructions, then submit for review there. Uploading an internal draft is not a public release. If review rejects it, open the exact policy link in the rejection and correct that issue before resubmitting. [Prepare the native build and identifiers](https://supastack.dev/docs/mobile/builds.md) [Expo's current store submission instructions](https://docs.expo.dev/submit/introduction/) [Apple's current App Review guidelines](https://developer.apple.com/app-store/review/guidelines/) [Google Play release instructions](https://support.google.com/googleplay/android-developer/answer/9859455) ``` corepack pnpm --filter @supastack/mobile exec eas login ``` ## Set up the accounts once Store submission needs three accounts: a free Expo account for EAS Build and Submit, a Google Play Console developer registration, and an Apple Developer Program membership for iOS. Install the EAS CLI and run eas login before building; both store enrollments can take days to verify, so start them early. Rename the application identifiers before the first upload — android.package and ios.bundleIdentifier ship as com.supastack.app in apps/mobile/app.json. Google Play locks the package name at first upload, so a wrong identifier means a new app entry rather than a rename. ## Let EAS manage signing apps/mobile/eas.json defines development, preview, and production build profiles. The production profile lets EAS generate and hold the Android keystore and iOS distribution certificate on its infrastructure — inspect them later with eas credentials, and keep local keystores out of the repository. Automated Play submission needs a Google Play service-account JSON key linked under Play Console → Setup → API access with release permission. Store it at apps/mobile/secrets/play-store-service-account.json — the path eas.json expects and a git-ignored directory. The first upload of a new app still happens manually in Play Console; eas submit takes over from the second release. ## Build, then submit Builds run on EAS infrastructure, so no Android Studio or Xcode install is needed for the binary. The production profile produces a store-ready .aab for Play and .ipa for the App Store with remote-managed version numbers. The preview profile produces an internal-distribution APK for physical-device QA. The shipped submit profile targets the Play internal track as a draft release — nothing reaches testers until you promote it inside Play Console after review passes. ``` corepack pnpm --filter @supastack/mobile build:android corepack pnpm --filter @supastack/mobile submit:android corepack pnpm --filter @supastack/mobile build:ios corepack pnpm --filter @supastack/mobile submit:ios # Internal-testing APK for a physical device: corepack pnpm --filter @supastack/mobile build:android:preview ``` ## Fill the Play Console forms Play review gates on declared content as much as the binary: a public privacy policy URL, the Data safety form, the content-rating questionnaire, target audience, and category. Answer Data safety from the app's real behavior — the kit collects account email for authentication, stores session tokens in platform secure storage, and talks only to your configured API; add whatever your product adds on top. For an iOS app that requires sign-in, provide App Review with a working demo account and instructions in the review notes. Review Apple's current login-services rule and its exceptions before choosing social sign-in options; the requirements depend on the app's actual login flow. The official guidelines are linked in the first-submission section above. ## Gate purchases behind RevenueCat first When your app offers mobile purchases, configure the shipped RevenueCat flow: set `MOBILE_PURCHASES_PROVIDER=revenuecat` and `EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=revenuecat`, then provide the matching public iOS or Android SDK key. The native app supports purchase and restore; after either, access refreshes from the server-verified workspace entitlement. Stripe Checkout is not used in the mobile flow. Keep the paywall unavailable when purchases are off or required configuration is missing. Validate configured store products, purchase, restore, and entitlement refresh in an EAS development build with store test accounts. Check current store guidance for requirements that apply to your app and regions. ## Pre-submission checklist Before submitting, confirm every item below using your own test app and store accounts. - android.package and ios.bundleIdentifier renamed to an identifier you own - EXPO_PUBLIC_API_URL in the build environment points at the production API - Privacy policy hosted at a public URL and linked in the store listing - Data safety form matches real collection: auth email, API traffic, anything your product adds - Working demo-account credentials in the App Store review notes - Paywall hidden, or backed by a verified RevenueCat entitlement - Deep-link return, session restore, and sign-out verified on a physical device - Screenshots and listing assets prepared for each store ## Recover from a rejection Rejections arrive with a policy reference — fix the declared cause rather than guessing at the symptom. A Data safety mismatch usually means the form is wrong, not the app. A login-required rejection usually means the review notes lacked a demo account. Resubmit with release notes that name the fix plainly. The full walkthrough — credential commands, console order of operations, and a rejection playbook — lives in docs/mobile-store-submission.md in the repository. ## Setup and configuration Shipping native needs Apple and/or Google developer accounts, app identifiers, signing credentials, and store review. The production EAS profile increments store build numbers; submit.production targets a draft release on Google Play's internal track. Bundle identifiers and the app scheme live in apps/mobile/app.config.ts. Web deploys and store releases move at different speeds — keep the API compatible with the oldest supported build, or gate new capability behind server-driven state. ``` corepack pnpm --filter @supastack/mobile build eas build --profile production --platform ios eas build --profile production --platform android eas submit --profile production --platform ios eas submit --profile production --platform android ``` ## Implementation workflow Set the RevenueCat project, store products and entitlement before enabling native purchases. Put the server secret key and webhook Authorization value only in server environment configuration; the iOS and Android SDK keys are public build values. Production must keep sandbox access disabled. Keep store metadata — screenshots, description, privacy answers, and purchase disclosures — in release materials owned by the product. RevenueCat purchases and restores are checked against current server subscriber state before the app shows access. ## Verification and troubleshooting Test the exact artifact you submit: sign-in callback, session restore, purchase and restore, server entitlement refresh, workspace switching, deep links, and offline behavior on a physical device. A successful local export is not a store-ready build. Record build number, store track, review status, and the API environment each release targets. A rejected review is a release event — fix metadata or capability flags rather than guessing at code. Next surface: the Desktop tab walks the same product through the Tauri shell. --- Source: [Run desktop in 5 minutes](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) # Run desktop in 5 minutes Prepare Rust and your operating system's build tools, start a reachable Web backend, launch the Tauri window and verify tray, shortcut and local task behavior. ## 1. Install the general and native tools Complete Before you start, clone your Desktop repository or All Platforms monorepo, and open it in the editor. Desktop is a native window around the same Web app. It does not include a second implementation of accounts or chat. The five-minute launch begins after dependencies and native tools are installed; first-time installation and Rust compilation often take longer. On Windows install Microsoft C++ Build Tools using the Tauri prerequisites link below. In its installer select Desktop development with C++, keep the Windows SDK selected and complete installation. Ensure Microsoft Edge WebView2 Runtime is installed. Install Rust from rustup.rs using rustup-init.exe; keep the default MSVC toolchain. Reopen your terminal after installation. On macOS, run xcode-select --install in Terminal and complete the command-line tools dialog if they are not installed. Install Rust using the macOS command from rustup.rs. On Linux, follow the Tauri prerequisite page for your exact distribution's WebKitGTK, compiler, tray and system-library packages, then install Rust. Linux package names vary; do not run another distribution's commands. Open a terminal at the repository root and run the version checks below. Both rustc and cargo must print versions; cargo is Rust's build tool. If either is missing, finish the installation and reopen the terminal before continuing. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) [Tauri's operating-system prerequisite checklist](https://tauri.app/start/prerequisites/) [Official Rust installer](https://rustup.rs/) ``` rustc --version cargo --version corepack pnpm install ``` ## 2. Start or choose the Web backend If your checkout contains apps/web, follow the Web tutorial, create apps/web/.env.local, run the Web server in a separate terminal using the command below and leave it running. First check http://localhost:3000 in your browser. You can use the /dashboard demonstration without a database; real sign-in at /auth needs migrated Postgres. A Desktop-only checkout does not contain apps/web. Use the separately running Web backend supplied for your product, and open its URL in your browser to confirm it works. The desktop shell cannot create the backend for you. Do not try the Web command below if that folder is absent. Step 3 shows how to choose a provided backend address. [Run a local Web backend](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Prepare real accounts with Postgres](https://supastack.dev/docs/web/database.md) ``` # Only when this checkout contains apps/web: corepack pnpm --filter @supastack/web dev ``` ## 3. Launch the desktop window Open a second terminal at the repository root while the backend stays running. For the default local backend at http://localhost:3000, run the first command below. Cargo may download libraries and compile the shell; wait for the Supastack window to open. Compiler messages during the first build are expected. For a provided backend, replace https://app.example.com in the appropriate operating-system block below with its actual URL. Run only your platform's block. SUPASTACK_APP_URL is a terminal environment setting, read when the native process starts. Putting it only in root .env will not configure the shell. Keep the backend URL free of embedded usernames or passwords. In the All Platforms monorepo, corepack pnpm dev:desktop can start Web and Tauri together once pnpm is on PATH as explained in Before you start. The filtered command below also works in Desktop-only checkouts, with the backend running separately. ``` # Default local backend (all operating systems): corepack pnpm --filter @supastack/desktop dev # Custom backend — Windows PowerShell only: $env:SUPASTACK_APP_URL = 'https://app.example.com' corepack pnpm --filter @supastack/desktop dev # Custom backend — macOS / Linux only: SUPASTACK_APP_URL=https://app.example.com corepack pnpm --filter @supastack/desktop dev ``` ## 4. Check the window, tray and shortcut Success means the window displays the same homepage as your backend in the browser. The desktop window has no browser address bar. To open the credential-free demonstration, quit the shell using the tray's Quit action, then run your operating system's block below in the desktop terminal while the Web server stays running. This loads /dashboard directly; follow the Web tutorial's local task steps there. Real account sign-in uses the backend's database, just as in the browser. Close the window using its close button: the app should hide to the tray rather than exit. Click the Supastack tray icon or press Ctrl + Shift + Space on Windows/Linux, or Command + Shift + Space on macOS, to show it again. Use the tray's Quit action to exit. To end development completely, also press Ctrl + C in the desktop terminal and then stop the backend terminal. If the window is blank or shows a connection problem, first open the configured URL in a browser. Start the backend or correct the URL, then restart Tauri. If Rust reports linker.exe not found, install the Windows C++ workload and reopen the terminal. If Linux reports a missing system library, follow the distribution-specific prerequisites. If the hotkey is already taken, use the tray and follow Native behavior to change it. This launches a local development window; it does not produce a signed installer or publish an app. Follow Desktop distribution for installer builds, platform signing and testing on another machine. Windows compilation does not verify macOS or Linux behavior. [Configure native window and shortcut behavior](https://supastack.dev/docs/desktop/run.md) [Build and verify desktop installers](https://supastack.dev/docs/desktop/release.md) ``` # Open the local demo — Windows PowerShell only: $env:SUPASTACK_APP_URL = 'http://localhost:3000/dashboard' corepack pnpm --filter @supastack/desktop dev # Open the local demo — macOS / Linux only: SUPASTACK_APP_URL=http://localhost:3000/dashboard corepack pnpm --filter @supastack/desktop dev ``` ## Use the local app URL Without an override, Tauri loads http://localhost:3000. Set SUPASTACK_APP_URL only if your local Next.js app uses another reachable HTTP(S) origin. Credential-bearing or non-web URLs are rejected. The development bundle identifier and release configuration are not production-ready settings. Keep the release preflight and platform signing for the separate release guide. --- Source: [Desktop overview](https://supastack.dev/docs/desktop.md) # Desktop overview A Tauri webview shell around the same web app — presence, not a second product. ## Beginner walkthrough Prerequisites: Read the two beginner guides below and finish Desktop local setup, including Rust and operating-system prerequisites. The code block starts both the web app and Tauri in the monorepo; in a desktop-only checkout, use its filtered command only after starting a reachable web backend separately. Tauri loads that backend. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Follow Desktop local setup, then open the app in the Tauri window and sign in to the same web account you use in a browser. 2. Visit a workspace page, hide/show the window from the tray, and try the summon shortcut. 3. The screen should match the web app because Tauri loads the configured backend. If it is blank, confirm that `SUPASTACK_APP_URL` points to a reachable running web app. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ``` # Monorepo: start the web app and desktop shell together corepack pnpm dev:desktop # Desktop-only checkout: start Tauri after a web backend is already reachable corepack pnpm --filter @supastack/desktop dev ``` ## One product UI, a native shell `apps/desktop` is a Tauri webview over your web app. It shares product UI, authentication and chat with web; native window/tray behavior lives in `src-tauri/src/lib.rs`. It needs a reachable web backend and does not bundle an independent product interface. ## Use the native shell features The shell ships a tray menu, close-to-tray, single-instance focus, global summon hotkey, native navigation/view menus, external links in the system browser, `supastack://` deep-link summon, remembered window state and an offline Retry overlay. `SUPASTACK_APP_URL` chooses the backend: valid runtime value, then compile-time value, then localhost. `SUPASTACK_DESKTOP_HOTKEY` overrides `CmdOrCtrl+Shift+Space`. Signed release builds inject `SUPASTACK_UPDATER_ENDPOINT` and `SUPASTACK_UPDATER_PUBKEY`; when the endpoint path contains `{{channel}}`, the app menu enables **Beta Updates** (`stable` vs `beta`, overridable at runtime with `SUPASTACK_UPDATE_CHANNEL`). ## Choose your desktop path Run web and desktop together for local work, then build and verify the native target with its platform prerequisites. Store signing, installers and physical desktop behavior require separate acceptance. [Set up Tauri desktop in 5 minutes](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Window, tray and hotkey behavior](https://supastack.dev/docs/desktop/run.md) [Desktop builds and distribution](https://supastack.dev/docs/desktop/release.md) ## A webview shell, not a second app apps/desktop is a Tauri 2.x shell. The Rust core in src-tauri owns the window, the system tray, and the global shortcut, while a webview loads the web application. Interface, authentication, billing, and AI all come from the same Next.js codebase — the desktop surface adds presence, not a parallel product. The shell ships a placeholder page in apps/desktop/ui that shows while the web app loads, so a cold start never renders a blank window. ## Point the shell at a deployment The window target resolves in order: a runtime SUPASTACK_APP_URL environment variable, then a compile-time SUPASTACK_APP_URL, then http://localhost:3000. Only http and https URLs are accepted — empty or non-web values fall back to the next candidate. For development this means the shell attaches to whatever corepack pnpm dev serves. For production, set the variable at build time to bake in your deployed web app. ## Keep secrets server-side The shell carries no provider credentials. The webview talks to the same authenticated API surface a browser uses, so provider keys, session secrets, and billing configuration stay on the server. The bundle identifier com.supastack.desktop and the default capability set (core:default) keep the webview sandboxed; widen permissions in src-tauri/capabilities only when a native feature needs them. --- Source: [Run and configure the shell](https://supastack.dev/docs/desktop/run.md) # Run and configure the shell Launch the Tauri dev shell and point it at your web app. ## Beginner walkthrough Prerequisites: Read the two beginner guides below and finish Desktop local setup, including Rust and operating-system prerequisites. The code block starts both the web app and Tauri in the monorepo; in a desktop-only checkout, use its filtered command only after starting a reachable web backend separately. Tauri loads that backend. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Open `apps/desktop/src-tauri/src/lib.rs`; ask your agent to change only the named window/tray/hotkey behavior and keep product UI in the web app. 2. Run the shell and test close-to-tray, Quit, and `CmdOrCtrl+Shift+Space`; test `supastack:///settings/appearance?preview=1#main`. 3. It should focus the existing window and preserve path/query/fragment. If it opens the wrong page, compare the configured origin and exact route. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ``` # Monorepo: start the web app and desktop shell together corepack pnpm dev:desktop # Desktop-only checkout: start Tauri after a web backend is already reachable corepack pnpm --filter @supastack/desktop dev ``` ## Keep shell behavior in Rust `src-tauri/src/lib.rs` creates the main 1200×800 webview window. Closing hides it to the tray; Quit exits. Native menus provide Home/Back/Forward/Reload, zoom/fullscreen and window actions. Window-state restoration preserves size/position. ## Handle links and summon The shell accepts `supastack://` routes and resolves them against the exact configured HTTP(S) app origin. For example, `supastack:///settings/appearance?preview=1#main` becomes `/workspace/settings?tab=billing#plans`; `supastack:///auth/callback?code=...` maps to `/auth/callback?code=...`. Query strings and fragments are preserved. Startup links are applied before the first window is shown. macOS runtime links use Tauri's `on_open_url`; Windows and Linux second-instance arguments use the single-instance callback with its `deep-link` feature enabled. Valid links focus and navigate the existing window. Only credential-free HTTP(S) links with the exact configured scheme, host, and effective port stay in the shell. Credential-free HTTP(S), `mailto:`, and `tel:` links may open externally; malformed links, credentials, unsupported schemes, ports on custom links, and traversal attempts are rejected. The configured backend URL also rejects credentials and falls back to the next valid/default origin. ``` SUPASTACK_APP_URL=https://your-app.example SUPASTACK_DESKTOP_HOTKEY=CmdOrCtrl+Shift+Space supastack:///settings/appearance?preview=1#main # Opens https://your-app.example/workspace/settings?tab=billing#plans ``` ## Verify platform-specific behavior The injected offline overlay offers Retry, hides on online and reloads the web app. Test focus, shortcuts, menu zoom, tray and deep-link invocation on every target OS; unit URL checks alone do not prove those native interactions. [Desktop acceptance](https://supastack.dev/docs/desktop/release.md) ## Setup and configuration Install the workspace dependencies and the Rust toolchain plus Tauri's per-OS prerequisites (WebView2 ships with Windows; macOS and Linux need their documented packages). corepack pnpm --filter @supastack/desktop dev runs tauri dev: it builds the Rust core, shows the placeholder UI, and loads the resolved app URL. SUPASTACK_APP_URL is read at runtime first and falls back to the value baked in at build time. Credential-free HTTP(S) values are accepted; otherwise the next candidate is used. SUPASTACK_DESKTOP_HOTKEY overrides the default CmdOrCtrl+Shift+Space toggle; an invalid value falls back to the default rather than failing startup. ``` corepack pnpm --filter @supastack/desktop dev corepack pnpm --filter @supastack/desktop typecheck SUPASTACK_APP_URL=https://app.example.com \ SUPASTACK_DESKTOP_HOTKEY=CmdOrCtrl+Shift+Space \ corepack pnpm --filter @supastack/desktop dev ``` ## Implementation workflow The main window stays on the exact configured origin, including scheme and port. Credential-free HTTP(S), mailto, and tel links can open in the system browser; unapproved schemes are cancelled. Registered supastack:// links map their route, query, and fragment onto the configured app origin. Startup and platform open-url/second-instance callbacks use the same validation, so a deep link cannot redirect the shell to another host. The tray icon and the global shortcut show and focus the single main window. When the shortcut does not register, another application usually owns the combination — pick a different SUPASTACK_DESKTOP_HOTKEY. ## Verification and troubleshooting Run cargo check and cargo test through the package scripts after touching src-tauri. Then verify exact-origin navigation, allowed external URL opening, a supastack:// route opening on the configured origin, tray and hotkey behavior, and sign-in persistence across restarts. --- Source: [Create desktop release accounts](https://supastack.dev/docs/desktop/external-accounts.md) # Create desktop release accounts Apple signing and notarization credentials, Windows code-signing options, and the hosted backend a released shell needs. ## When you need these Local desktop work needs nothing on this page — `corepack pnpm --filter @supastack/desktop dev` runs the shell, and `tauri build` produces unsigned installers that work fine on your own machine. These accounts matter only before a public release: code signing and Apple notarization are what remove the security warnings other people's computers show for an unknown app. Create them when a release is close, not on day one. [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/web/aws-ses.md) ## Apple: certificate + notarization key 1. Enroll in the [Apple Developer Program](https://developer.apple.com) — $99 per year. The same membership that submits iOS apps covers macOS signing; you do not pay twice. 2. Create a Developer ID Application certificate on a Mac: Xcode → Settings → Accounts → your account → Manage Certificates → + → Developer ID Application (Keychain Access's certificate-assistant flow is the alternative). 3. Notarization — Apple's server-side check that Gatekeeper trusts — uses an App Store Connect API key: open [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → Users and Access → Integrations → App Store Connect API → generate a key, download the `.p8` file immediately (it is offered once), and note the Key ID and Issuer ID. Those credentials feed the `APPLE_*` environment names documented in Code signing — the certificate, its password, the signing identity, and the API key/issuer pair. Keep every one in a secret store, never in the repo. [Wire the credentials into the build](https://supastack.dev/docs/desktop/signing.md) ## Windows code signing Two options exist: buy an OV or EV code-signing certificate from a certificate authority — EV removes SmartScreen warnings fastest while OV builds reputation over time — or use Microsoft's Azure Trusted Signing service with an Azure account, which is the newer subscription route. Unsigned installers still install and run; they trigger SmartScreen warnings for anyone who downloads them. That is acceptable for private testing and unacceptable for a public release. The certificate thumbprint or a custom `signtool` command goes into the Windows bundle configuration — the details live in Code signing, this page only covers the account. [Configure Windows signing](https://supastack.dev/docs/desktop/signing.md) ## A public backend URL The desktop shell is a webview, not a standalone app: a release build must load a public HTTPS backend, which you provide by baking `SUPASTACK_APP_URL` into the build. A shell that points at `localhost` is a local tool, not a product. Deploy the web app first — the Google Cloud walkthrough produces the HTTPS URL, and the deployment checklist is the shorter reference. Set `SUPASTACK_APP_URL` to it when building the release. [Deploy the backend to Google Cloud](https://supastack.dev/docs/web/deploy-google-cloud.md) [Deployment checklist](https://supastack.dev/docs/web/deployment.md) ## An update endpoint Tauri's updater checks an HTTPS `latest.json` manifest to learn about new versions. Any static host works — a GitHub Releases release or a file in an S3/R2 bucket — so an existing GitHub account is enough to start; no separate service is required. That is account-level preparation only: the updater wiring itself is a release-time concern for the Bundle and release work, not something to configure now. ## Done means Check each item before release work: Apple shows an active Developer Program membership, a Developer ID Application certificate sits in Keychain, and the App Store Connect `.p8` is saved with its Key ID and Issuer ID recorded. On Windows a certificate path is chosen — CA purchase or Trusted Signing — or you have explicitly decided to ship unsigned for now. The backend check: a public HTTPS deployment exists and `SUPASTACK_APP_URL` for release builds points at it. The updates check: a host for `latest.json` is picked — GitHub Releases or a bucket. Then continue in order: [Bundle and release](https://supastack.dev/docs/desktop/release.md) [Code signing](https://supastack.dev/docs/desktop/signing.md) [Deploy the shared backend](https://supastack.dev/docs/web/deployment.md) --- Source: [Bundle and release](https://supastack.dev/docs/desktop/release.md) # Bundle and release Produce installers and keep release evidence separate from a successful build. ## Beginner walkthrough Prerequisites: Finish Desktop local setup, including Rust and the operating-system build tools. Stop the Tauri development process before building an installer; leave the Web backend running. Run each check below from the repository root and wait for it to finish before running the next. The filtered package commands work in Desktop-only and All Platforms checkouts. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. In a local branch, ask your coding agent to inspect `apps/desktop/src-tauri/tauri.conf.json` and set the product identifier, app name, icons, and backend URL to values you own. Use a public HTTPS backend URL for release builds. 2. From the repository root, run the Desktop package checks below. Before check:release-config, set SUPASTACK_APP_URL in this terminal using the operating-system command in Desktop local setup; the public HTTPS backend must be running, and the app identifier must be your own. 3. A successful build does not sign, notarize, or publish an installer; if Rust or a system library is missing, finish Desktop local setup. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ``` corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop check:release-config corepack pnpm --filter @supastack/desktop build ``` ## Configure the product shell Review `src-tauri/tauri.conf.json`, Cargo metadata, capabilities and platform identifiers. Replace app icons through the shared asset process and set the intended backend URL at build/runtime. The hosted web app must already be deployed and reachable. ## Run the native build gate These package commands run Rust typecheck and tests, validate release settings, and build the Tauri installer. Signing and distribution configuration depend on the target platform and your account/certificates; do not store signing material in source. ``` corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop check:release-config corepack pnpm --filter @supastack/desktop build ``` ## Verify the installer on the target OS Check clean install, launch, backend reachability, auth, menus/tray, hotkey registration, second instance, deep links, offline retry and uninstall. Verify each supported OS separately. A local build does not prove signed or notarized commercial distribution; signing configuration, an updater feed, and release automation are not included. [Native acceptance checklist](https://supastack.dev/docs/desktop/run.md) [Deploy the shared backend](https://supastack.dev/docs/web/deployment.md) ## Setup and configuration Run corepack pnpm --filter @supastack/desktop check:release-config before preparing an installer. It checks that SUPASTACK_APP_URL is credential-free HTTPS on a public host and that the Tauri identifier is product-specific; the check is opt-in and does not block local development. corepack pnpm --filter @supastack/desktop build runs tauri build. The bundle targets in tauri.conf.json produce the installer formats available on the host OS — build on each OS you ship. Artifacts land under apps/desktop/src-tauri/target/release/bundle/. productName, version, and identifier live in tauri.conf.json — bump the version there before cutting a release. Regenerate icons with corepack pnpm --filter @supastack/desktop icons, which rewrites src-tauri/icons from icon-source.png. ``` corepack pnpm --filter @supastack/desktop check:release-config corepack pnpm --filter @supastack/desktop icons corepack pnpm --filter @supastack/desktop build # artifacts: apps/desktop/src-tauri/target/release/bundle/ ``` ## Implementation workflow Unsigned builds install locally but trigger OS trust warnings. Code signing and Apple notarization belong in your release configuration — provision certificates through your own secret store, never in the repo. Bake the production SUPASTACK_APP_URL into release builds so installed shells point at the deployed web app rather than localhost. ## Verification and troubleshooting Install the produced bundle on each target OS and record the build, OS, and app URL used for acceptance. Verify launch, the loaded deployment, tray and hotkey behavior, and sign-in persistence — a completed tauri build alone is not release evidence. --- Source: [Code signing](https://supastack.dev/docs/desktop/signing.md) # Code signing Sign installers so macOS and Windows stop warning your users. ## Beginner walkthrough Prerequisites: Finish Desktop local setup, including Rust and the operating-system build tools. Stop the Tauri development process before building an installer; leave the Web backend running. Run each check below from the repository root and wait for it to finish before running the next. The filtered package commands work in Desktop-only and All Platforms checkouts. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. In a local branch, ask your coding agent to inspect `apps/desktop/src-tauri/tauri.conf.json` and set the product identifier, app name, icons, and backend URL to values you own. Use a public HTTPS backend URL for release builds. 2. From the repository root, run the Desktop package checks below. Before check:release-config, set SUPASTACK_APP_URL in this terminal using the operating-system command in Desktop local setup; the public HTTPS backend must be running, and the app identifier must be your own. 3. A successful build does not sign, notarize, or publish an installer; if Rust or a system library is missing, finish Desktop local setup. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ``` corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop check:release-config corepack pnpm --filter @supastack/desktop build ``` ## Configure the product shell Review `src-tauri/tauri.conf.json`, Cargo metadata, capabilities and platform identifiers. Replace app icons through the shared asset process and set the intended backend URL at build/runtime. The hosted web app must already be deployed and reachable. ## Run the native build gate These package commands run Rust typecheck and tests, validate release settings, and build the Tauri installer. Signing and distribution configuration depend on the target platform and your account/certificates; do not store signing material in source. ``` corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop check:release-config corepack pnpm --filter @supastack/desktop build ``` ## Verify the installer on the target OS Check clean install, launch, backend reachability, auth, menus/tray, hotkey registration, second instance, deep links, offline retry and uninstall. Verify each supported OS separately. A local build does not prove signed or notarized commercial distribution; signing configuration, an updater feed, and release automation are not included. [Native acceptance checklist](https://supastack.dev/docs/desktop/run.md) [Deploy the shared backend](https://supastack.dev/docs/web/deployment.md) ## Setup and configuration 1. Unsigned installers install fine on your own machine but trigger Gatekeeper and SmartScreen warnings for everyone else — sign before you ship. 2. On macOS, enroll in the Apple Developer Program for a Developer ID certificate, then add the signing and notarization config under the bundle section of src-tauri/tauri.conf.json and feed Tauri its documented signing env (APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, plus the APPLE_API_KEY / APPLE_API_ISSUER notarization credentials) from your CI secret store. 3. On Windows, configure the bundle's Windows signing — a certificate thumbprint or a custom signtool command — with the certificate delivered through your runner's secret store as well. ``` # tauri.conf.json ships unsigned today: # "bundle": { "active": true, "targets": "all", "icon": [...] } # Add signing under bundle > macOS / bundle > windows # and keep every certificate and password in your secret store, # never in the repo. # Auto-updater & release channels (stable / beta via {{channel}}): # SUPASTACK_UPDATER_ENDPOINT=https://releases.example.com/{{channel}}/{{target}}/{{arch}}/{{current_version}} # SUPASTACK_UPDATER_PUBKEY=… # SUPASTACK_UPDATE_CHANNEL=stable ``` ## Implementation workflow Provision certificates through your own secret store and inject them as environment variables at build time — a certificate committed to the repo is a certificate to revoke. Bake the production SUPASTACK_APP_URL into the same release build so installed shells point at the deployed app. ## Verification and troubleshooting Verify by installing the produced artifact on a machine that isn't yours: no Gatekeeper or SmartScreen warning, the app launches, and sign-in persists. A green tauri build without the signed install test is not release evidence. Next: the Web tab's Deployment checklist covers the server-side launch gates. --- Source: [Module overview](https://supastack.dev/docs/modules.md) # Module overview Small contracts that make the infrastructure easier to understand and reuse. ## Read the foundation in plain language Authentication checks who signed in. A session remembers that sign-in. A workspace keeps a person's or team's records together. Billing records payments; an entitlement grants access to a feature. A job runs work outside a page request. Transactional email sends messages triggered by app actions, such as sign-in codes, rather than marketing outreach. An adapter connects one service to the same app interface. Local or fake adapters let you explore without provider accounts. When you need a real service, select one provider and follow its setup guide; you do not need to configure every module at once. Begin with database and sign-in, then enable payments, AI and background work only as your product needs them. [Start with persistent data](https://supastack.dev/docs/web/database.md) [Understand real sign-in](https://supastack.dev/docs/modules/auth.md) [Find the setup guide for a feature](https://supastack.dev/docs/web/project-structure.md) ## Compose the capabilities you need The shared packages provide identity, workspace persistence, billing, AI, background jobs, transactional email, and observability. Applications compose these modules; product domain logic stays outside Starter Core. Prefer the existing contract when integrating a feature. If it cannot describe the behavior you need, make that limitation explicit before expanding it. ## Follow ownership through the flow A session identifies a user. A workspace scopes their records. Entitlements authorize capabilities. A background task carries trusted context into execution. Usage and audit records let the application account for the work afterward. ## Use local adapters for deterministic checks Fake/local adapters support repeatable tests without paid provider requests. Real providers are verified through a separately configured smoke path. An adapter test is evidence about the contract, not proof of real delivery or persistence. --- Source: [Environment and config](https://supastack.dev/docs/modules/config.md) # Environment and config One validated schema for the whole workspace. ## Make one configuration change and check it Start with a running Web app. In the editor open apps/web/.env.local; if it does not exist, copy .env.example there using the Web tutorial. For a harmless first check, find EMAIL_PROVIDER=local and leave it local. This setting sends product email to the local preview instead of a real inbox. Do not paste KEY=value lines into the terminal. For each service you enable, follow that provider's setup guide, replace only the named values, save the file, stop the Web server with Ctrl + C and restart with the command below. If validation names a missing key, reopen this file and compare that key with the provider guide. Keep root .env synchronized only for values used by database migrations; copying it alone does not configure Next.js. [Look up a configuration key](https://supastack.dev/docs/web/project-structure.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/web/terminal-basics.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Start with the environment example The root .env.example is the configuration inventory. Shared validation lives in packages/config. Review the selected application environment and provider modes together; a local interface is not a production credential strategy. The credential-free reference mode requires local/test application configuration and fake/local providers. Selecting a production provider changes the runtime requirements. ## Keep secrets behind the API Authentication secrets, database credentials, provider keys, Stripe secrets, and Trigger credentials are server-only. Never prefix them with NEXT_PUBLIC_ or EXPO_PUBLIC_. Do not print them during setup or include them in error responses. The browser receives public configuration and allowed model aliases. Workspace ownership, entitlement decisions, budgets, and provider selection remain server-enforced. ## Check a change before using it Validate configuration through the existing config package and run the relevant deterministic tests. Use the separately opted-in provider smoke path when you are ready to verify real integrations. ``` corepack pnpm test corepack pnpm check corepack pnpm build ``` ## Setup and configuration packages/config is the single place that parses process.env: a zod schema produces a typed ServerConfig and PublicConfig, and provider selection is expressed as kinds — providers.ai, providers.billing, providers.email, providers.jobs, providers.storage each resolve to a named adapter plus its required fields. Enabling a provider makes its credentials required: turning on a social sign-in, the s3 storage kind, or the resend email kind fails startup with the missing variable named, rather than a runtime surprise later. ``` STORAGE_PROVIDER=s3 STORAGE_BUCKET=kit STORAGE_ENDPOINT=https://s3.example STORAGE_PUBLIC_BASE_URL=https://cdn.example # → config rejects the env if a required field is missing ``` ## Implementation workflow Read config through parseServerConfig at the server boundary and pass the typed value inward — do not touch process.env inside feature code. Public config is a separate, deliberately small object that is safe to serialize to the client. Product-specific env belongs to the consuming product's own schema. Resist growing the kit schema for one product's needs. ## Verification and troubleshooting Run the config package tests after changing the schema, and check that a missing required variable names itself in the error. An env that parses locally but not in production usually means an undocumented required field. --- Source: [i18n module](https://supastack.dev/docs/modules/i18n.md) # i18n module The locale contract every surface shares. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration packages/i18n owns the locale set and resolution contract: SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, isAppLocale, resolveLocale, localePath, and getDictionary. Web routes, mobile strings, and transactional mail all read through the same package. Resolution order is path prefix → cookie → Accept-Language → default. localePath(locale, path) produces the canonical href — un-prefixed for the default locale, /{locale}/… otherwise — and the proxy layer enforces it. ``` import { getDictionary, localePath, resolveLocale } from "@supastack/i18n"; const locale = resolveLocale([pathLocale, cookie, acceptLanguage]); const copy = getDictionary(locale).marketing; ``` ## Implementation workflow Add a user-facing string to the dictionary types first, then every locale — the translation completeness suite fails on missing keys and on English text left untranslated. Never branch product logic on locale; dictionaries are presentation. Format dates, numbers, and plurals through Intl APIs with the resolved locale rather than string templates. ## Verification and troubleshooting Run the i18n package tests plus the site-content completeness suite after touching keys. Spot-check a locale-prefixed route, the language switcher, and Accept-Language detection with a cleared cookie. --- Source: [Workspaces](https://supastack.dev/docs/modules/workspaces.md) # Workspaces Scope every user-owned record by workspaceId. ## Try a personal workspace and a team A workspace is a separate container for accounts, content and permissions. Complete database-backed sign-in first. Open the dashboard and note the current workspace name. Use the workspace switcher to choose another workspace you belong to; the displayed records should change to that workspace's records. To try a team, follow Organizations to create one, invite a second test email and accept that invitation in a private browser window signed in as that second account. Confirm a member sees that team's records while an unrelated account cannot open them. Owner and admin roles manage the team; member access is narrower. Keep the two accounts in separate browser sessions during this test. If you ask an agent to add a new product table or screen, explicitly request workspaceId on every user-owned record and server-side membership checks for every read and write. A workspace ID in a URL identifies the workspace; it does not grant access. Have the agent test an unrelated account before using the screen with real customer data. [Create a team and invite members](https://supastack.dev/docs/web/organizations.md) [Verify records stay private to their workspace](https://supastack.dev/docs/web/security.md) ## Resolve the workspace from the session Personal workspaces give each user an ownership boundary. The server resolves the current workspace from trusted authentication context. Client-supplied IDs must never replace that check. ## Scope reads and writes Include workspaceId in every user-owned lookup or mutation. A detail route needs the same isolation as a list route. Task IDs, recovery URLs, and idempotency keys identify work but do not authorize access. Apply this rule to your product tables as well as the shared task, usage, and provider configuration records. ## Test cross-workspace denial Create two independent workspace contexts. Verify that the owner can read and change their record, the other workspace cannot, and an anonymous request is rejected. Confirm rejected writes leave the persisted record unchanged. --- Source: [Authentication](https://supastack.dev/docs/modules/auth.md) # Authentication Identity and sessions through Better Auth. ## Create and verify a real account Authentication means proving who is using the app. Start Postgres and run migrations using the database guide first, then leave the Web server running. Open http://localhost:3000/auth, choose email/password sign-up, enter your name, email and password, and submit. Keep EMAIL_PROVIDER=local for this first test. When a flow requests an email code, open http://localhost:3000/mail-preview in another browser tab, find the most recent message for that address and copy its code back into the sign-in form. Request a fresh code if it expires. These messages stay in the local outbox; repeatedly checking an external inbox will not find them. Sign out, then sign in again to confirm the database-backed account works. If sign-up reports a database connection or missing-table error, finish database setup instead of enabling social login. Google, GitHub, Apple, X and Microsoft buttons need their own provider application and callback settings; enable one only after its provider guide is complete. Better Auth is the shipped default engine and does not need a paid authentication account for local email sign-in. [Start Postgres before creating an account](https://supastack.dev/docs/web/database.md) [Use and troubleshoot email sign-in codes](https://supastack.dev/docs/web/setup-ai.md) [Configure Google sign-in](https://supastack.dev/docs/web/setup-oauth.md) [Add Google sign-in](https://supastack.dev/docs/web/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/web/github-sign-in.md) ## Choose enabled sign-in methods The foundation includes email/password, OTP, and configurable social authentication. Better Auth owns identity and sessions, while the application composes provider configuration and transactional OTP delivery. Enable only the social providers you have configured. Provider client secrets and the authentication secret stay on the server. ## Connect identity to a workspace Authenticated handlers resolve session state before accessing workspace records. Workspace provisioning must remain idempotent so replaying a lifecycle hook does not create duplicate ownership records. ## Verify session behavior Test sign-in, invalid or expired OTPs, single-use consumption, sign-out, and rejected anonymous access. Memory-adapter tests cover handler semantics; production database persistence and real provider callbacks need separate acceptance. --- Source: [Billing and access](https://supastack.dev/docs/modules/billing.md) # Billing and access Separate payment events from authorization decisions. ## Choose and test your payment service Billing handles payments. Access permissions are stored separately as entitlements. Complete persisted account setup, then open /billing in the running Web app to see the plan interface. With BILLING_PROVIDER=local, you are exploring local behavior rather than collecting real money. Choose one supported payment provider, follow its setup guide, create your own test product and price, and copy the named server settings into apps/web/.env.local. Save and restart the Web server. Complete one checkout using that provider's sandbox instructions, then inspect the workspace's access state after the provider's webhook arrives. A webhook is the provider's signed message to your server about a payment event. A success page alone does not unlock features. If checkout completes but access remains inactive, check the provider's webhook delivery and your server's event processing before repeating payment. Test cancellation and expired access as well. Phone purchases use the separate RevenueCat/store setup, not this Web checkout. [Choose a billing adapter and its settings](https://supastack.dev/docs/modules/billing-configuration.md) [Set your own products and prices](https://supastack.dev/docs/web/project-structure.md) [Set up Stripe in a test environment](https://supastack.dev/docs/web/setup-payments.md) ## Web billing The Web application supports the billing provider you select with BILLING_PROVIDER; Stripe is one option. Open /billing for Web plans and subscription management. Follow the selected provider's guide to set its server-only credentials, product or price IDs and webhook verification settings in the matching test or live environment. ## Authorize through entitlements A checkout redirect is not evidence of active access. The server-owned entitlement decides whether a workspace can use a paid capability. Check that entitlement at the action boundary even when the interface already hides the action. ## Verify the lifecycle Use sandbox events to verify activation, updates, cancellation, and webhook handling. Keep repeated events safe and ensure an inactive entitlement blocks paid operations. Native mobile purchases remain a separate RevenueCat integration boundary. --- Source: [Stripe configuration and webhooks](https://supastack.dev/docs/modules/billing-configuration.md) # Stripe configuration and webhooks Connect web subscriptions to server-owned entitlements. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local and keep BILLING_PROVIDER=local while learning the checkout screen. 2. For a real integration, create test-mode products in one provider, then add its server keys and webhook secret to the Web host's private environment settings. A webhook is a signed message the billing service sends to your app after checkout, renewal, or cancellation; the endpoint path is listed in this guide's code example. 3. Start Web with corepack pnpm --filter @supastack/web dev and test only the sandbox. Expected: a verified webhook updates the signed-in workspace's entitlement; opening a checkout page alone does not grant access. Use sandbox/test credentials while integrating. Configure BILLING_PROVIDER plus the provider's server-only secrets: stripe → STRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET; lemonsqueezy → LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_STORE_ID, LEMONSQUEEZY_VARIANT_ID, LEMONSQUEEZY_WEBHOOK_SECRET; creem → CREEM_API_KEY, CREEM_PRODUCT_ID, CREEM_WEBHOOK_SECRET (CREEM_TEST_MODE=true for the sandbox); polar → POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, POLAR_WEBHOOK_SECRET (POLAR_SERVER); whop → WHOP_API_KEY, WHOP_PLAN_ID, WHOP_WEBHOOK_SECRET; paddle → PADDLE_API_KEY, PADDLE_PRICE_ID, PADDLE_WEBHOOK_SECRET (PADDLE_ENVIRONMENT); dodo → DODO_PAYMENTS_API_KEY, DODO_PRODUCT_ID, DODO_PAYMENTS_WEBHOOK_KEY (DODO_PAYMENTS_ENVIRONMENT); paypal → PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_PLAN_ID, PAYPAL_WEBHOOK_ID (PAYPAL_ENVIRONMENT). The price/product must represent the intended recurring subscription. ``` Webhook endpoint: /api/billing/webhook (stripe), /api/billing/webhook/lemonsqueezy, /api/billing/webhook/creem, /api/billing/webhook/polar, /api/billing/webhook/whop, /api/billing/webhook/paddle, /api/billing/webhook/dodo, /api/billing/webhook/paypal Checkout endpoint: /api/billing/checkout Portal endpoint: /api/billing/portal ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Follow the lifecycle event list in docs/real-provider-smoke.md when configuring the webhook endpoint. Signature verification requires the endpoint's matching webhook secret. A checkout redirect alone must never grant access. The entitlement at the server action boundary decides whether paid work is permitted. Process repeated events safely and scope customer/subscription records to the correct workspace. Web checkout remains separate from native purchases. The key a plan grants is product vocabulary shared across surfaces: ProductPlan.entitlement in src/product-plans.ts declares the lowercase machine key written into the ledger while a plan is paid (the kit ships 'ai.reference-task' on pro and lifetime; src/billing-runtime.ts passes it to the BFF). The same key is what a CMS entry's required_entitlement field and the EntitlementGate component's entitlementKey prop equality-compare against — an entry gated on 'ai.reference-task' unlocks for any workspace holding that grant, while a key no plan grants stays locked for everyone. ## Verification and troubleshooting If a webhook is rejected, compare the exact sandbox URL and signing secret in the provider dashboard with the server settings, and check whether that provider requires forwarding requests to localhost. If checkout works but access stays locked, inspect the webhook result and the entitlement key on the plan. Never test with live payment credentials while following this local guide. Test activation, updates, cancellation, invalid signatures, and repeated events in the sandbox. Inspect persisted access state after processing. Creating an unconsumed checkout session proves connectivity only, not lifecycle fulfillment. --- Source: [Database module](https://supastack.dev/docs/modules/database.md) # Database module The Drizzle schema and persistence helpers every record flows through. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Install and start Docker Desktop. 2. Copy .env.example to .env in the root for migration tools and to apps/web/.env.local for Next.js (PowerShell: Copy-Item .env.example .env; Copy-Item .env.example apps/web/.env.local. macOS/Linux/Git Bash: cp .env.example .env; cp .env.example apps/web/.env.local). Keep DATABASE_URL the same in both files; the example already matches the local Docker database. 3. From the repository root run docker compose up -d postgres and docker compose ps; wait until postgres reports healthy. 4. Run corepack pnpm --filter @supastack/db db:migrate from the root to apply the migrations already shipped with the kit. Only after editing packages/db/src/schema.ts, run db:generate, inspect the generated SQL, then run db:migrate. Expected: the local Postgres container is healthy and the Web app uses its migrated database. Never point this first run at production. Configure DATABASE_URL for the intended Postgres database in the server environment. Use a separate disposable database for integration acceptance. Never aim test migrations at a production database. The database workspace exports schema definitions and persistence helpers. Review packages/db/src/schema.ts and the migration directory before introducing a table or column. ``` docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate # Only after changing packages/db/src/schema.ts: corepack pnpm --filter @supastack/db db:generate corepack pnpm --filter @supastack/db db:migrate ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Choose one copy method for your terminal: # PowerShell: Copy-Item .env.example .env Copy-Item .env.example apps/web/.env.local # macOS/Linux/Git Bash alternative: # cp .env.example .env # cp .env.example apps/web/.env.local docker compose up -d postgres docker compose ps corepack pnpm --filter @supastack/db db:migrate ``` ## Implementation workflow Generate a migration after editing the schema, inspect the generated SQL, and commit it with the schema change. Plan how existing records receive values before adding a required column. Include workspaceId on every user-owned record and carry that scope into reads and writes. Use immutable model-price history for AI accounting instead of rewriting prices used by existing usage records. ## Verification and troubleshooting If Docker says it cannot connect to its engine, start Docker Desktop and wait for it to finish loading. If postgres is not healthy, run docker compose logs postgres from the root and fix that error before migrating. If Web says DATABASE_URL is missing, check apps/web/.env.local; if db:migrate cannot connect, check the root .env. If you change databases, update both files. Read the first failed migration statement before retrying; never repeat it against production. Apply migrations to an isolated database and run database-gated tests there. Missing credentials should produce an explicit skipped gate, not an assertion that persistence passed. If migration fails, inspect which statement failed and the current schema before retrying. --- Source: [Storage module](https://supastack.dev/docs/modules/storage.md) # Storage module One ObjectStorage contract, memory and S3-compatible adapters. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. For a no-credential trial, keep `STORAGE_PROVIDER=memory`. If this checkout has no visible upload control, ask your coding agent to inspect `apps/web/app/api/storage/avatar/route.ts`, `apps/web/app/api/storage/logo/route.ts`, `apps/web/app/api/storage/organization-logo/route.ts`, and the matching settings page before wiring a small sample image. 2. To check persistence, set `STORAGE_PROVIDER=s3`, `STORAGE_REGION`, and `STORAGE_BUCKET` in `apps/web/.env.local`, add server-only test credentials locally, and restart. 3. The image should reload from storage. Memory files disappear when the process restarts; for S3 errors, check bucket, endpoint, and access policy. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Select the storage backend `STORAGE_PROVIDER=memory` is the credential-free default. `local` writes objects to disk under `STORAGE_LOCAL_DIR` (default `./.data/storage`, required explicitly in production) and streams them through HMAC-signed expiring URLs at `/api/storage/local/[...key]` (`STORAGE_LOCAL_SIGNING_SECRET`, falling back to `AUTH_SECRET`) for single-server deployments. `s3` uses `apps/web/src/storage-runtime.ts` to compose the AWS SDK client over the narrow `@supastack/storage` contract, supporting Supabase Storage and MinIO with path-style URLs enabled by default. ``` STORAGE_PROVIDER=s3 STORAGE_REGION=us-east-1 STORAGE_BUCKET=your-product-bucket # Set STORAGE_ACCESS_KEY_ID and STORAGE_SECRET_ACCESS_KEY on the server. # Optional: STORAGE_ENDPOINT, STORAGE_FORCE_PATH_STYLE, STORAGE_PUBLIC_BASE_URL # Self-hosted single-server disk storage: # STORAGE_PROVIDER=local # STORAGE_LOCAL_DIR=./.data/storage # STORAGE_LOCAL_SIGNING_SECRET= ``` ## Reuse the upload boundaries The shipped routes cover objects, avatars, profile logos and organization logos under `/api/storage/`. They resolve identity/workspace permissions and validate upload shape before storage. Inspect the chosen route's size/type policy rather than accepting arbitrary client object keys. Public URLs are presentation, not authorization. Keep protected content behind the application's verified read boundary and configure bucket/CDN policy for your product's data. ## Test real storage behavior Test allowed/denied uploads, MIME and size rejection, foreign-workspace paths, missing objects and provider failure. Memory mode does not persist across processes; verify storage and retrieval against the selected backend before release. [Supabase Storage setup](https://supastack.dev/docs/web/project-structure.md) [Upload route inventory](https://supastack.dev/docs/web/project-structure.md) ## Setup and configuration packages/storage defines the ObjectStorage interface — putObject, getObject, and deleteObject against object locators — with MemoryObjectStorage for deterministic tests and S3CompatibleObjectStorage for production buckets. Object keys are built with buildWorkspaceObjectKey and checked with assertWorkspaceObjectKey, so every stored file is namespaced by workspace and purpose (avatar, file, logo). WorkspaceScopedStorageError fires when a key escapes its workspace. ``` interface ObjectStorage { putObject(input: PutObjectInput): Promise getObject(locator: ObjectLocator): Promise deleteObject(locator: ObjectLocator): Promise getPublicUrl(locator: ObjectLocator): string } key = {workspaceId}/{purpose}/{safeName} ``` ## Implementation workflow Application routes resolve the workspace, validate the upload, build the scoped key, and call the adapter — storage never sees a raw client path. Select the adapter in the server environment through STORAGE_PROVIDER and its credentials. Expose reads through the same authorization boundary; only serve public URLs for objects the product deliberately publishes. ## Verification and troubleshooting MemoryObjectStorage keeps unit tests deterministic — assert keys, content types, and cross-workspace denials there. Verify the S3 adapter through the opted-in smoke path against a real bucket before shipping uploads. --- Source: [API module](https://supastack.dev/docs/modules/api.md) # API module The versioned public contract, key plumbing, and OpenAPI surface. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration packages/api is the source of truth for the public REST surface: zod schemas for requests, responses, and the error envelope, plus the OpenAPI document (openApiDocument) served at /api/v1/openapi. Route handlers compose these schemas instead of redefining shapes. API keys are issued per workspace with a sk__ prefix, stored hashed with constant-time comparison, and carry scopes — read by default. api-keys.ts owns creation, lookup, and revocation. ``` apiV1 = "/api/v1" schemas: currentWorkspace, entitlement, modelRequirements, createAiTaskRequest, aiTask, aiTaskResult, apiError openApiDocument → GET /api/v1/openapi ``` ## Implementation workflow Add endpoints inside the existing version: extend schemas, keep the error envelope, and regenerate the OpenAPI surface so /api/docs stays accurate. Breaking changes get a new version, not a silent shape change. Resolve the caller's workspace from the presented key, apply the same entitlement and validation rules as the web app, and rate-limit per key. ## Verification and troubleshooting Contract-test every route shape against the package schemas. Verify unknown keys, revoked keys, and underscoped requests all fail with the documented error envelope, and that the OpenAPI document stays valid JSON. --- Source: [Rate limiting](https://supastack.dev/docs/modules/rate-limiting.md) # Rate limiting One limiter for every abuse-sensitive route. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. In the server environment, choose `RATE_LIMIT_STORE=postgres` for shared production buckets and set `PUBLIC_AI_DAILY_BUDGET_MICROUNITS=0` to keep public AI disabled. 2. Set `TRUSTED_PROXY_COUNT` only to the exact number of controlled proxies that append forwarding headers; do not copy `1` unless that is true for your host. 3. Verify a denied request returns a retry-safe error. If limits differ across instances, check they share Postgres. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ## Choose the limiter store `RATE_LIMIT_STORE` is memory or postgres. When unset, production selects Postgres and other environments select memory. Shared `rate_limit_bucket` rows coordinate multiple servers; store failures currently fail open to preserve API availability. Workspace/user AI submission limits and the public API/MCP limiter protect their separate surfaces. Preserve the existing caller-specific bucket keys when adding routes. ## Trust only controlled proxy hops `TRUSTED_PROXY_COUNT` ranges from 0–10 and counts reverse proxies that append `x-forwarded-for`. Zero trusts none and uses a shared unknown bucket; one controlled proxy typically needs 1. An incorrect count lets untrusted forwarded values affect the limiter. ## Limit anonymous provider spend `PUBLIC_AI_DAILY_BUDGET_MICROUNITS=0` disables public AI calls. A configured nonnegative daily UTC cap reserves platform spend before provider calls and is separate from each workspace's budget. Test concurrent reservations, reset boundaries and denial before any billable provider attempt. ``` RATE_LIMIT_STORE=postgres TRUSTED_PROXY_COUNT=1 PUBLIC_AI_DAILY_BUDGET_MICROUNITS=0 ``` ## Setup and configuration rate-limit-runtime provides shared limiters wired into the abuse-sensitive surfaces — apiV1Limiter (600 requests/minute) and authLimiter (300 requests/minute) are the shipped presets. Limits key on identity plus route bucket and answer with a retryable 429 carrying Retry-After rather than a generic error. Extend the existing limiter to new abuse-sensitive endpoints — sign-in, OTP, checkout, task submission, key creation — instead of inventing per-route counters that drift apart. ``` export const apiV1Limiter = createNamedRateLimiter("api-v1", 600, 60_000); export const authLimiter = createNamedRateLimiter("auth", 300, 60_000); ``` ## Implementation workflow Pick the key carefully: per-user for authenticated actions, per-API-key for public traffic, per-IP only as a last resort for anonymous endpoints. Legitimate bursts get a Retry-After; attacks hit a hard ceiling. Keep the limiter fail-closed on abuse-sensitive routes — if the store is unreachable, prefer rejecting expensive work over letting it through unbounded. ## Verification and troubleshooting Exercise a limiter until it returns 429 with a sane Retry-After, verify different workspaces get independent buckets, and confirm a burst resolves back to success after the window. Log denials without the request's sensitive payload. --- Source: [AI and background jobs](https://supastack.dev/docs/modules/ai.md) # AI and background jobs From an authorized request to a durable result. ## Try fake AI, then choose a real provider For your first result, follow the Web tutorial's /dashboard task: start the local flow, queue a prompt, run it and view the result. AI_PROVIDER=fake returns a repeatable demonstration response without sending data to an external model or using a paid API key. For real generation, first configure persisted accounts, choose an AI provider and follow AI configuration below. Add a model and its price to the database catalog, choose the permitted models for each alias, and grant the workspace the required access. An alias such as fast is a friendly choice shown to the user; it is not itself an external provider's model ID. A background AI task also needs the configured jobs service. If no model choices are shown, inspect the catalog and alias policy before retrying. If access is denied, inspect the active workspace's entitlement. Keep all provider API keys in the Web server's configuration, not the browser, mobile app or desktop window. [Try the credential-free AI demonstration](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Configure real AI generation](https://supastack.dev/docs/modules/ai-configuration.md) [Add allowed models and prices](https://supastack.dev/docs/web/setup-ai.md) [Configure background task execution](https://supastack.dev/docs/modules/jobs.md) ## Follow the task lifecycle The production task API validates the authenticated workspace and entitlement before persisting a task and dispatching the ai.generate Trigger.dev workflow. The router applies capability requirements and configured model policy. ``` POST /api/ai/tasks → authenticated workspace + entitlement → durable task record → Trigger.dev ai.generate → model routing + generation → usage record + result → transactional completion delivery ``` ## Configure aliases and budgets The user selects an enabled alias: cheap, fast, or smart. The server controls the underlying model IDs, provider credentials, workspace policy, and budget enforcement. Encrypted bring-your-own-key support must preserve the same server-only boundary. ## Keep workflow semantics explicit JobRunner promises enqueue, status, and cancel only. Trigger.dev workflow semantics are explicit, including durable execution behavior. Do not infer retries or scheduling guarantees from the generic contract. The production submission path uses idempotency and saved task recovery to handle ambiguous dispatch. Completion of generation is separate from completion-email delivery. --- Source: [AI providers and model policy](https://supastack.dev/docs/modules/ai-configuration.md) # AI providers and model policy Configure server-owned providers and the aliases exposed to users. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local for the local Web server and apps/web/src/agent-runtime.ts for provider selection; .env.example is the list of supported setting names. 2. Keep AI_PROVIDER=fake for local interface work unless you have your own provider account and want to test real generation; never put provider keys in a NEXT_PUBLIC_ or EXPO_PUBLIC_ setting. 3. Start the Web app from the repository root with corepack pnpm --filter @supastack/web dev. Expected: local screens can be explored without configuring a paid provider; actual generation requires a valid server provider and enabled model choices in the database catalog. The default AI_PROVIDER=fake supports local deterministic work. Real provider configuration belongs in the server environment. The provider keys listed in .env.example include OpenAI, Anthropic, and an OpenAI-compatible endpoint. AI_MODEL_POLICY contains ordered model IDs for cheap, fast, and smart aliases. An empty alias list disables that choice. Model IDs must correspond to enabled database catalogue entries with effective immutable pricing. ``` AI_MODEL_POLICY='{"cheap":[],"fast":["catalog-model-id"],"smart":["catalog-model-id"]}' ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Configure the catalogue and workspace policy before accepting production tasks. The browser receives enabled aliases, while capability matching, model selection, entitlement checks, and budget enforcement stay on the server. Keep credentials out of NEXT_PUBLIC_ and EXPO_PUBLIC_ variables. For bring-your-own-key mode, use the existing encrypted envelope and server decryption boundary; never return a stored provider key to the interface. ## Verification and troubleshooting If the app reports that no AI model is available, check the server's AI_PROVIDER, the enabled models and prices in the admin catalog, and the cheap/fast/smart alias policy. If a real provider returns an authentication error, check its key in apps/web/.env.local and restart the server. A key prefixed NEXT_PUBLIC_ would be visible to browsers and must be removed. Verify disabled aliases, missing capabilities, inactive entitlement, and exhausted budgets. Use a separately authorized provider smoke run for actual generation. If no alias is available, inspect server policy and catalogue configuration rather than exposing a raw model selector. --- Source: [Task API and recovery](https://supastack.dev/docs/modules/task-recovery.md) # Task API and recovery Recover an uncertain submission without creating a new task. ## Before you begin Open your downloaded supastack project in Visual Studio Code using File → Open Folder, then choose Terminal → New Terminal. The repository root is the folder containing package.json, apps and packages. Run the command below there and leave its terminal open. If a command is not recognized, finish the tool installation guide before continuing. Read the platform tutorial first. A server is the app process that answers browser or phone requests. Web reads apps/web/.env.local, Expo reads apps/mobile/.env.local, and database migration commands read root .env. The no-database /dashboard demonstration is separate from real /auth accounts, which need migrated Postgres. A phone or desktop-only checkout needs a separately running Web backend. [Install the tools and get the code](https://supastack.dev/docs/web/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/web/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/web/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web dev ``` ## Setup and configuration POST /api/ai/tasks requires an authenticated workspace and active entitlement. A keyed request reserves an immutable task before dispatch. Idempotency-Key accepts 16–128 ASCII letters, digits, underscores, or hyphens. ``` POST /api/ai/tasks Idempotency-Key: a-stable-client-submission-key Recovery body: { "resume": true } Status: GET /api/ai/tasks/{taskId} ``` ## Implementation workflow Persist the recovery key before submitting. After uncertain dispatch, reuse the same key and the resume body; the server loads the original input and retries the same task-scoped Trigger identity. Changed input under the same key returns 409 idempotencyConflict. HTTP 202 after an ambiguous enqueue means dispatch is uncertain. It does not prove Trigger accepted the work. Already running or terminal tasks return saved state without another enqueue. Recovery still requires authentication, ownership, and entitlement. ## Verification and troubleshooting Exercise a lost response, changed-input conflict, foreign-workspace request, and refresh restoration. There is no automatic outbox dispatcher or end-to-end exactly-once guarantee. Do not reset a running task to queued without investigating its side effects. --- Source: [Trigger.dev jobs](https://supastack.dev/docs/modules/jobs.md) # Trigger.dev jobs Keep durable workflow behavior explicit. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/src/job-handlers.ts and apps/web/src/scheduler-runtime.ts in VS Code. A job is background work; the scheduler decides when to enqueue it. 2. For a local tick, set the same SCHEDULER_TICK_SECRET in apps/web/.env.local and the repository-root .env; the tick script reads the root .env, while the Web endpoint reads its app settings. 3. Start Web with corepack pnpm --filter @supastack/web dev, then run corepack pnpm jobs:tick from the root. Expected: the terminal shows a POST result from localhost:3000. Trigger.dev is a separate provider and needs its own account and server keys. Set JOB_PROVIDER=trigger with server-only TRIGGER_SECRET_KEY and TRIGGER_PROJECT_REF for production task submission. Review apps/web/trigger.config.ts and the ai.generate task in apps/web/trigger before configuring the target project. ``` JOB_PROVIDER=trigger TRIGGER_PROJECT_REF=your-project-reference # Provide TRIGGER_SECRET_KEY through the server secret store. ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` # Terminal 1 — start the Web app from the monorepo root: corepack pnpm --filter @supastack/web dev # Terminal 2 — run one scheduler tick from the monorepo root: corepack pnpm jobs:tick ``` ## Implementation workflow The shared JobRunner contract promises enqueue, status, and cancel. Trigger.dev owns workflow-specific semantics; do not infer scheduling, retries, or durability guarantees from the generic interface. The durable application task ID connects dispatch, worker execution, usage records, and results. A failed provider-run metadata attachment after confirmed enqueue must not cause another enqueue or invalidate accepted work. ## Verification and troubleshooting If the tick returns 404, confirm the secret exists in both settings files and matches exactly, then restart Web. If the terminal says connection refused, start Web first. If no job ran, check that a schedule is enabled and due; one tick does not make a future schedule run immediately. Verify the deployed task identifier and project match the configured environment. Separate enqueue acceptance from workflow completion and from email delivery. Read docs/production-ai-runtime.md for recovery and safe diagnostics before manually retrying work. --- Source: [Scheduled tasks](https://supastack.dev/docs/modules/schedules.md) # Scheduled tasks Recurring workspace work through the schedule actions. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Web local setup, and open this monorepo at its root in your editor. Web settings belong in `apps/web/.env.local`; Next.js does not read the root `.env`. `/dashboard` in the default fake mode is a no-database demo; `/auth` needs a migrated database. Quick terms: a workspace is one team's separate users, data, and settings. The server is the trusted backend; keep provider keys out of browser and phone code. A migration is a versioned database change that creates or updates tables. An AI model is the engine that answers; the model catalog is the server's list of available model IDs and prices. A provider is an outside service such as an AI, email, or billing service. An entitlement is a grant that lets a workspace use a feature. A webhook is a message one service sends to another service's URL. 1. Open Settings → Developers and use the Schedules controls to create a test job with a UTC run time. 2. Keep the app running and make the same server-only `SCHEDULER_TICK_SECRET` available to the app and terminal; run the command below from the repository root. 3. Confirm the due time advances and the job reaches its own status. If nothing runs, check the UTC time, scheduler provider, and server logs; never paste the secret into chat. For custom code, start with the supplied `prompts/README.md` and copy the closest prompt: `prompts/new-app-screen.md` for an app screen or `prompts/new-landing-page.md` for a public page. For server, database, or provider work, give your coding agent this guide's task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md`, and matching `.ai/` mirrors; inspect the exact source files named here; list files and checks before editing; preserve workspace ownership and server-only secrets; and show you the diff. Tell it the behavior you want, input and output fields, allowed roles, expected success and errors, and the test account, provider, or device. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/web/before-you-start.md) [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md) [Web local setup](https://supastack.dev/docs/web/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/web/agents.md) ``` corepack pnpm jobs:tick ``` ## Store recurring work in the database Settings scheduling stores the job type and cron rule in workspace-scoped `job_schedule` rows. Cron fields are evaluated in UTC. `createDrizzleJobScheduler.claimDueSchedules` claims due rows using `FOR UPDATE SKIP LOCKED`, advances their next run, then dispatches handlers. The generic `JobRunner` promises enqueue/status/cancel. Recurrence belongs to scheduler rows; Trigger.dev workflow semantics stay explicit. ## Drive scheduler ticks Set a server-only `SCHEDULER_TICK_SECRET` to enable `POST /api/internal/scheduler-tick`. `corepack pnpm jobs:tick` calls it for dev/CI; absent configuration returns 404. Under `JOB_PROVIDER=trigger`, the deployed `scheduler.tick` cron drives the scheduler. ``` # Against a running local app, with SCHEDULER_TICK_SECRET configured: corepack pnpm jobs:tick ``` ## Verify claims and execution separately Test overlapping ticks, due/not-due rules, disabled rows, handler failure, cancellation and another workspace's schedule. Inspect claim/advance and job completion separately; a claimed row is not proof of completed work. [Shipped email.digest example](https://supastack.dev/docs/web/project-structure.md) [Trigger.dev execution](https://supastack.dev/docs/modules/jobs.md) ## Setup and configuration Scheduled work — digests, cleanups, renewals — goes through schedule-actions rather than ad-hoc timers: listWorkspaceSchedules, upsertScheduleAction, and removeScheduleAction manage a workspace's cron-driven tasks. A schedule names the task, the cron expression, and the workspace it runs under. The worker resolves context at run time, so a paused or deleted schedule stops producing work without a redeploy. ``` listWorkspaceSchedules() upsertScheduleAction({ task, cron }) removeScheduleAction(formData) ``` ## Implementation workflow Treat every scheduled run as untrusted input: re-resolve the workspace and entitlement inside the run, because state may have changed since registration. Make the task idempotent — a retried or doubly-fired run must not duplicate effects. Schedules are operational surface: keep them visible in settings or logs so a stuck or runaway schedule is found before users notice. ## Verification and troubleshooting Fire the schedule manually in a sandbox, pause it, verify a disabled workspace stops producing runs, and confirm a duplicated fire produces one effect. Keep cron expressions in managed configuration, not scattered through task code. --- Source: [Transactional email](https://supastack.dev/docs/modules/email.md) # Transactional email Delivery for authentication and product workflows. ## Read your first local message, then enable delivery Transactional email means a message triggered by an app action, such as a sign-in code or task result. With the Web server running, keep EMAIL_PROVIDER=local in apps/web/.env.local. Complete database setup, request a sign-in code at /auth, then open /mail-preview and inspect the latest message. A local message appearing here is not an email delivered to your external inbox. To send real messages, choose one email provider, create its account, verify a domain you control and follow its dedicated guide. Put the named API key and sender address in apps/web/.env.local, save and restart the Web server. Request one new sign-in code, then check both the provider's delivery log and the destination inbox, including spam. Domain verification and sender approval can take time. If the message fails, inspect the delivery error and sender verification before requesting repeatedly. The /mail-preview route is development-only and returns 404 in production. Keep marketing outreach separate from this transactional sender so a newsletter campaign cannot disrupt account verification mail. [Choose a mail provider and sender address](https://supastack.dev/docs/modules/email-configuration.md) [Enable Resend delivery](https://supastack.dev/docs/web/setup-email.md) ## Keep transactional delivery separate The email module handles transactional product messages, including authentication and task completion. Outreach belongs to the consuming product’s own workflow and must remain separate from this transactional path. ## Configure the delivery adapter Use the local adapter for deterministic tests. Configure the selected production adapter and its server-side credentials before enabling external delivery. Never place mail-provider secrets in client configuration or logs. ## Record delivery independently A generated result and its completion email have separate outcomes. Inspect delivery records before claiming a message was sent successfully. Test a failed delivery without treating it as a failed generation or submitting duplicate model work. --- Source: [Email provider configuration](https://supastack.dev/docs/modules/email-configuration.md) # Email provider configuration Configure transactional delivery and verify its independent outcome. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/web/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/web/terminal-basics.md). These instructions assume you opened the full supastack monorepo in VS Code; its root package.json marks the repository root. In pnpm commands, a workspace package is an app or shared-code folder; in the product, a workspace is a team's data container and security boundary. corepack pnpm --filter ... selects one package from the monorepo while commands run from its root. If you opened only a synced mobile or desktop folder, it does not contain the Web backend, Docker setup, or root scripts: run the backend from this monorepo or a reachable hosted server, and use the platform folder's own scripts. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the relevant app after changing it, and keep server secrets out of NEXT_PUBLIC_ and EXPO_PUBLIC_ values. 1. Open apps/web/.env.local and set EMAIL_PROVIDER=local for development; EMAIL_FROM is the sender label used by templates. 2. Start the Web app from the repository root with corepack pnpm --filter @supastack/web dev and trigger one sign-in or invite flow with a test address. Expected: the app records a local email outcome without contacting a real provider. Only connect Resend, SES, or another service after its sender domain is verified and test recipients are restricted. EMAIL_PROVIDER=local keeps deterministic delivery local. For configured production delivery, review the Resend or SES adapter and provide its server credentials. EMAIL_FROM must match the identity verified with the selected provider. ``` EMAIL_PROVIDER=local EMAIL_FROM=supastack # Production adapter configuration is listed in .env.example. ``` ## Commands to copy Follow the comments to see which folder a command runs in. If two terminals are labeled, open two terminal windows and keep the first command running while you use the second. ``` corepack pnpm --filter @supastack/web dev ``` ## Implementation workflow Authentication and completion messages use the transactional email boundary. Outreach workflows belong to the consuming product and remain separate. Never reuse this path as an unreviewed bulk messaging subsystem. Record delivery separately from generation. A model result can be available while its completion message fails. Recovery must inspect delivery state instead of generating the same result again. ## Verification and troubleshooting If no local email appears, check that EMAIL_PROVIDER is set in apps/web/.env.local and restart Web. If a real provider rejects a message, confirm EMAIL_FROM matches a verified sender and inspect the provider's delivery record. A generated AI answer can succeed even when its follow-up email fails; check delivery before running the AI task again. Verify recipient restrictions, sender identity, and provider errors in a dedicated test environment. A local adapter result is not evidence of real email delivery. The opt-in smoke path restricts recipients through REAL_SMOKE_EMAIL_ALLOWED_DOMAIN. --- Source: [Audit, analytics and errors](https://supastack.dev/docs/modules/observability.md) # Audit, analytics and errors Understand activity with workspace context and sanitized event data. ## Find useful information when an action fails Observability means seeing what your app did. Begin with the terminal running your Web server: repeat one failing action and read the first error and any correlation ID, a label linking messages from the same request. Share the command, time and sanitized error when asking for help; do not share .env files or tokens. For hosted error reports, follow Health and operations to configure the supported monitoring settings in apps/web/.env.local or the host's environment dashboard. Save and restart the server. Trigger a controlled test failure in a test environment and confirm it appears with the correct workspace context. Monitoring being configured does not prove it received an event. ALERT_WEBHOOK_URL is an optional HTTPS destination for job and terminal webhook-delivery failure alerts. If you need it, create a compatible incoming endpoint and save its URL server-side; otherwise leave it empty. Ask an agent to simulate one failure and confirm exactly one alert, with no secrets in its text. Do not trigger paid work or failures in customer accounts for this test. [Check application health and operational settings](https://supastack.dev/docs/web/security.md) [Diagnose an error in the local terminal](https://supastack.dev/docs/web/troubleshooting.md) ## Keep a workspace audit trail Audit records capture changes such as provider configuration updates within a workspace. Resolve the authenticated workspace before reading or writing these records, and preserve that scope when adding product-specific events. Record the action and identifiers needed to understand it. Provider credentials and other sensitive values must stay out of audit payloads and logs. ## Capture product events The observability package exposes an AnalyticsClient adapter and workspace-scoped AnalyticsEvent values. Each event carries a name, distinct identity, workspaceId, and optional properties. Use createObservability to route events through your configured analytics client. Structured properties are redacted before delivery; keep sensitive free-form text out of event names and property values as well. ## Report errors with safe context ErrorClient captures exceptions with workspaceId tags and structured extra context. The shared observability layer sanitizes exceptions and redacts sensitive context before forwarding them to the configured client. Use local adapters to verify captured events and exceptions without calling a monitoring provider. Test that workspace tags remain present and credentials are removed before enabling an external destination. ## Follow a request across tasks, jobs and webhooks Entry boundaries mint a corr_ correlation id that links the originating request to the work it triggers. AI-task submission stamps the id on the task payload and the ai_task_submitted event, every billing webhook receiver mints one per request, and the scheduler mints one per dispatched job. The same id rides job.completed and ai_task_completed event properties, captureException extras, the webhook_event ledger payload and each webhook_delivery payload meta — so it lands inside the signed body a subscriber receives. Missing or malformed ids are tolerated: the consumer mints a fresh id instead of rejecting older rows. ## Alert on real failures Set ALERT_WEBHOOK_URL to any HTTPS endpoint that accepts a JSON POST shaped {text, name, severity, context}; a Slack-style incoming webhook works as-is. Alerts are fire-and-forget with a five-second timeout, a failed delivery logs one console warning, and the alerter noops while the variable is unset. The shipped alerts are job.failed (warning — a job handler threw or a job type is unknown) and webhook.delivery_failed (critical — only when an outgoing delivery reaches terminal failure after its final attempt; ordinary retries stay quiet and each delivery row alerts at most once). ## Measure durations and stream structured logs serverObservability.measure times an async operation and emits a .duration analytics event carrying durationMs — on success and on failure, without masking the original error. The ai.task.execute job run is already wrapped, so task execution duration lands in the configured analytics client. serverObservability.logger writes one JSON line per record to stdout with environment and service base fields; the shared redaction layer strips secrets before any line is emitted. --- Source: [Skills](https://supastack.dev/docs/skills/overview.md) # Skills Ready-made instructions that teach your coding agent how to do common supastack jobs the way the kit expects. ## What skills are supastack ships Agent Skills: short, task-focused instructions your coding agent loads when a job matches. Instead of guessing the kit's conventions on every prompt, the agent follows the same steps for features, payments, database changes and tests. Skills follow the open Agent Skills format, so they work in Claude Code, Cursor, Codex, GitHub Copilot, Gemini CLI and other compatible agents. ## Where skills live Skills live in `.agents/skills/`, one folder per skill with a `SKILL.md` file inside. Identical copies sit in `.claude/skills/` for Claude Code. ## Which agents read them Codex, Cursor, GitHub Copilot, Gemini CLI and OpenCode read `.agents/skills/`. Claude Code reads `.claude/skills/`. Your agent picks a skill automatically when your request matches its description, or you can name it — say “use the `database-schema-change` skill”. ## Which repositories ship them The all-platforms repository includes every skill. The web, mobile and desktop repositories include the skills that apply to them; end-to-end testing and CI skills ship only with the all-platforms repository. ## Setup & environment Skills for getting the kit running, deployed and configured. - **Local environment setup** (`local-environment-setup`) — use when bootstrapping or repairing the local environment: first clone, a missing or broken .env, Postgres, migrations or ports. - **Deploy and env vars** (`deploy-and-env-vars`) — use when deploying, choosing a hosting target, or adding or changing an environment variable. - **Adding a dependency** (`adding-a-dependency`) — use when adding, upgrading, moving or removing an npm dependency, or when CI fails the license or audit check. - **Upgrading from upstream** (`upgrading-from-upstream`) — use when pulling a newer supastack release into a product built on it. ## Building features Skills for adding product surface area on each platform. - **Add a web feature** (`add-a-web-feature`) — use when adding a product page or section with its own data, server actions, navigation and tests. - **Add an API endpoint** (`add-an-api-endpoint`) — use when adding or changing a public /api/v1 endpoint, its OpenAPI documentation or API-key scopes. - **Add a UI component** (`add-a-ui-component`) — use when building a section, block, form control or reusable component that follows the kit's styling rules. - **Change the theme or style** (`change-the-theme-or-style`) — use when changing how the product looks: a design style, color pack, density, radius, font or layout. - **Add a marketing page or blog post** (`add-a-marketing-page-or-blog-post`) — use when adding or editing a public page, pricing page, legal page, blog post or docs guide. - **Add a mobile screen** (`add-a-mobile-screen`) — use when adding or changing a screen in the Expo mobile app. - **Add a desktop capability** (`add-a-desktop-capability`) — use when changing the Tauri desktop app: window, tray, global shortcut or auto-updates. - **Add a notification** (`add-a-notification`) — use when adding an in-app notification, push notification or digest item. ## Auth & payments Skills for sign-in, billing and plans. - **Add an auth method** (`add-an-auth-method`) — use when changing sign-in: social providers, magic links, email codes, passkeys or two-factor. - **Add a payment provider** (`add-a-payment-provider`) — use when activating a billing provider or writing a new billing adapter with checkout, customer portal and webhooks. - **Change plans and pricing** (`change-plans-and-pricing`) — use when adding, renaming or repricing a plan: prices, trials, seats, entitlements, usage limits or credits. ## Data & integrations Skills for the schema, AI, jobs, email, storage and translations. - **Database schema change** (`database-schema-change`) — use when adding or changing a table, column, index or relation and generating the migration. - **Add an AI feature** (`add-an-ai-feature`) — use when adding a model call, chat or agent tool, a durable AI task, budgets or bring-your-own-key. - **Add a background job** (`add-a-background-job`) — use when adding work that runs after a request, on a timer or per workspace. - **Add or edit an email** (`add-or-edit-an-email`) — use when adding or changing a transactional email or switching the provider that delivers it. - **Use file storage** (`use-file-storage`) — use when adding uploads or stored files such as avatars, logos or attachments. - **Add translations** (`add-translations`) — use when adding or changing user-facing text, translating it or adding a locale. ## Quality & shipping Skills for tests, verification, reviews and CI. - **Writing unit tests** (`writing-unit-tests`) — use when writing or fixing Vitest unit or integration tests. - **Writing e2e tests** (`writing-e2e-tests`) — use when adding or fixing Playwright end-to-end tests, locally or in CI. - **Verify changes** (`verify-changes`) — use before handing off, committing or opening a pull request to pick and run the right checks. - **Debugging a failing CI run** (`debugging-a-failing-ci-run`) — use when a GitHub Actions run fails and you need to find and fix the cause. - **Reviewing a PR** (`reviewing-a-pr`) — use when reviewing a pull request or diff for correctness, workspace isolation and conventions. ## Add your own Add a folder whose name matches the `name` in its frontmatter, with a `name` and a `description` that says when to use it. Copy it to `.claude/skills/` too, so every agent can find it. --- Source: [supastack vs TurboStarter](https://supastack.dev/blog/supastack-vs-turbostarter.md) # supastack vs TurboStarter Both ship web and mobile from one monorepo. supastack adds desktop, API keys, webhooks and 144 test files; TurboStarter adds a browser extension. A side-by-side look. _Published 2026-09-23._ ## TL;DR supastack and TurboStarter are the two starters that go beyond the web. Both are pnpm monorepos with a Next.js web app and an Expo mobile app on top of shared TypeScript packages, and both use Better Auth and Drizzle. The difference is in what else you get and how much of it is tested. - Pick supastack if you are building a B2B or AI product that needs team workspaces, workspace API keys, outbound webhooks, an audit log, bring-your-own AI keys with budgets, or a desktop app, and you want a codebase with a real test suite. - Pick TurboStarter if a browser extension is part of your product, or you want more payment providers out of the box. ## At a glance | | supastack | TurboStarter | |---|---|---| | Web | Next.js 16.3 | Next.js 16.3 | | Mobile | Expo 57 (iOS + Android) | Expo 57 (iOS + Android) | | Desktop | Yes (Tauri) | No | | Browser extension | No | Yes (WXT) | | Auth | Better Auth: email OTP, password, magic link, OAuth, 2FA, passkeys, admin impersonation and bans; Auth.js option | Better Auth: organizations, 2FA, passkeys, magic link, email OTP, anonymous, admin | | Team workspaces | Personal + organization workspaces, invites, owner/admin/member roles | Organizations with invitations | | Web billing | Stripe or Lemon Squeezy, per-seat | Stripe, Polar, Lemon Squeezy, Dodo Payments | | Mobile billing | RevenueCat scaffolded behind the paywall | RevenueCat, Superwall | | AI | Provider router, encrypted BYOK keys, per-workspace usage budgets, model price catalog | AI module with an OpenAI demo | | Background jobs | Trigger.dev (enqueue / status / cancel + cron) | Not a core module | | Developer platform | Workspace API keys, signed outbound webhooks, audit log | Not built in | | Content | Workspace CMS (posts, pages, docs) | CMS package | | Commerce | Products, orders, inventory, refunds | No | | Compliance | GDPR export, session management | No dedicated module | | Feature flags | Entitlement gates | Feature-flag package per platform | | Languages | en, de, es, fr | en, es | | Test files in repo | 144 | 15 | | TypeScript files | ~470 | ~950 | ## Where supastack is stronger Tests you can trust — supastack's repository holds 144 test files and a credential-free pnpm verify:local gate that runs the whole golden path with fake providers. You can change billing or auth code and know quickly whether you broke something. TurboStarter ships 15. Business plumbing for B2B and AI products — workspace API keys, signed outbound webhooks, an audit log, GDPR export and admin impersonation are already there. If your customers will call your product from their own systems, you would otherwise build all of that yourself. AI with guardrails — supastack stores each workspace's own provider keys encrypted, routes requests through a capability-aware router, and enforces usage budgets against a price catalog. TurboStarter gives you a demo endpoint. Desktop — a Tauri app sits next to web and mobile. Smaller surface — roughly half the TypeScript files of TurboStarter, so there is less to read before you start. ## Where TurboStarter is stronger Browser extension — TurboStarter ships a WXT extension app that shares auth, API and UI packages with web and mobile. If a Chrome or Firefox extension is part of your product, this saves weeks. More payment options — Polar and Dodo Payments on web, plus Superwall on mobile. Feature flags per platform and an anonymous sign-in mode. ## Who should pick which | You are building | Pick | |---|---| | B2B SaaS where customers integrate via API or webhooks | supastack | | AI product where each team brings its own model keys | supastack | | Web + mobile + desktop app | supastack | | Product with a companion browser extension | TurboStarter | | Consumer app that needs Polar, Dodo or Superwall | TurboStarter | ## Verdict Both are serious, current starters. TurboStarter covers the most client surfaces thanks to its extension app. supastack goes deeper on the server side, with teams, API keys, webhooks, audit and AI budgets, and backs it with a far larger test suite. For most B2B and AI SaaS products, that depth matters more than one more client. Comparison based on the source code of both starters as of September 2026. Features change quickly, so check each project's docs before you buy. --- Source: [supastack vs MkSaaS](https://supastack.dev/blog/supastack-vs-mksaas.md) # supastack vs MkSaaS MkSaaS is a polished Next.js starter with credits and Chinese support. supastack adds mobile, desktop, team workspaces, API keys and webhooks. Here is how they compare. _Published 2026-09-23._ ## TL;DR MkSaaS is one of the most polished single-app Next.js starters: credits, blog, docs, newsletter and English plus Chinese, with a family of real products built on it. supastack is a monorepo foundation for products that need more than a website: mobile and desktop apps, team workspaces and a developer platform. - Pick supastack if you need mobile or desktop apps, team workspaces, API keys and webhooks for your customers, or a large automated test suite. - Pick MkSaaS if you are launching a web-only AI tool fast, want a credits system and Chinese out of the box, or plan to deploy on Cloudflare. ## At a glance | | supastack | MkSaaS | |---|---|---| | Structure | Monorepo: web, mobile, desktop, shared packages | Single Next.js app | | Web | Next.js 16.3 | Next.js 16.3 | | Mobile / desktop | Expo (iOS + Android), Tauri desktop | No | | Auth | Better Auth: OTP, password, magic link, OAuth, 2FA, passkeys, admin | Better Auth: password, OAuth, admin impersonation | | Team workspaces | Yes, with invites and roles | No | | Billing | Stripe or Lemon Squeezy, per-seat | Stripe, Creem, Waffo | | Credits system | No (entitlements instead) | Yes | | AI | Provider router, encrypted BYOK keys, usage budgets | AI SDK (OpenAI, DeepSeek) | | API keys + outbound webhooks | Yes | No | | Audit log, GDPR export | Yes | No | | Blog / docs / newsletter | CMS + docs site | Blog, docs, newsletter | | Languages | en, de, es, fr | en, zh | | Deploy | Vercel, Docker, self-host | Vercel, Docker, Cloudflare branch | | Test files in repo | 144 | 18 (+ Playwright per payment provider) | ## Where supastack is stronger More than a website — Expo mobile and Tauri desktop apps share auth, billing and API with web. MkSaaS is web only. Teams — personal and organization workspaces with invites and owner/admin/member roles, and per-seat billing. MkSaaS has no organizations. A developer platform — workspace API keys, signed outbound webhooks and an audit log let your customers integrate with you from day one. AI you can bill safely — each workspace can bring its own provider keys, stored encrypted, and usage is capped by budget. Test depth — 144 test files plus a credential-free local verification gate. ## Where MkSaaS is stronger Credits — a ready credits system for pay-per-use AI tools. Chinese and English — both languages ship with the template. Marketing surface — blog, docs, newsletter and many landing blocks are ready. Cloudflare — separate branches for Cloudflare Workers with Postgres or D1. Proven by real products — the author runs several live products on MkSaaS, and their code is available to buyers as examples. ## Who should pick which | You are building | Pick | |---|---| | Web-only AI tool with pay-per-use credits | MkSaaS | | Product for Chinese-speaking users | MkSaaS | | B2B SaaS with teams and seat pricing | supastack | | Product with a mobile or desktop app | supastack | | API-first product with webhooks | supastack | ## Verdict MkSaaS is the faster path to a web-only, credit-based AI tool, especially for a Chinese audience. supastack is the better foundation when the product needs teams, mobile or desktop clients, or integrations, and when you want tests to catch regressions as the codebase grows. Comparison based on the source code of both starters as of September 2026. Check each project's docs for the latest features. --- Source: [supastack vs ShipAny](https://supastack.dev/blog/supastack-vs-shipany.md) # supastack vs ShipAny ShipAny wires in many AI model providers with credits, RBAC and affiliate tools. supastack brings teams, mobile, desktop, BYOK budgets, API keys and 144 test files. A side-by-side comparison. _Published 2026-09-23._ ## TL;DR ShipAny is a popular AI SaaS starter, especially in the Chinese indie community. Template Two, its current generation, ships many AI providers, credits, role-based admin and growth extras. supastack focuses on a tested foundation with teams, integrations and multiple clients. - Pick ShipAny if you are launching a web AI generation site and want many model providers, credits, PayPal and affiliate tools ready. - Pick supastack if you need teams, mobile or desktop apps, API access for customers, AI cost controls and a tested codebase. ## At a glance | | supastack | ShipAny Template Two | |---|---|---| | Structure | Monorepo: web, mobile, desktop | Single Next.js app | | Web | Next.js 16.3 | Next.js 16.0 | | Mobile / desktop | Expo, Tauri | No | | Auth | Better Auth with 2FA, passkeys, OTP, admin | Better Auth | | Teams | Organization workspaces with invites | No (RBAC roles and permissions for admins) | | AI providers | Router with OpenAI and Anthropic, extendable | fal, Gemini, Kie, Replicate, OpenRouter | | BYOK keys + budgets | Yes | No | | Credits | No | Yes | | Billing | Stripe or Lemon Squeezy, per-seat | Stripe, Creem, PayPal | | Growth extras | Announcements, notifications | Affiliate, ads, customer-service widgets | | API keys | Workspace API keys + signed webhooks | API keys | | Languages | en, de, es, fr | en, zh | | Test files in repo | 144 | 0 | ## Where supastack is stronger Tests — 144 test files and a local verification gate. The ShipAny version we reviewed has none, so every customization is checked by hand. Teams — organization workspaces with invites and seat billing. AI cost control — encrypted per-workspace keys and usage budgets. Integrations — signed outbound webhooks and an audit log alongside API keys. Mobile and desktop apps. ## Where ShipAny is stronger AI breadth — five model platforms are wired in, including OpenRouter for hundreds of models, plus an admin view of AI tasks. Credits and growth tools — credits, affiliate programs, ads and live-chat widgets are ready. Payments for global and Asian markets — PayPal and Creem. Chinese community and docs. ## Who should pick which | You are building | Pick | |---|---| | AI image, video or chat site sold with credits | ShipAny | | Product for Chinese-speaking users | ShipAny | | AI product for teams, with cost controls | supastack | | Product with API integrations or webhooks | supastack | | Product with mobile or desktop apps | supastack | ## Verdict ShipAny is a fast launcher for credit-based AI generation sites. supastack is the sturdier foundation for AI SaaS that serves teams, integrates with other systems and needs to change safely over time. Comparison based on the source code of both starters as of September 2026 (ShipAny Template Two; Template One is its previous generation). Check each project's docs for the latest features. --- Source: [supastack vs Indie Kit Pro](https://supastack.dev/blog/supastack-vs-indie-kit.md) # supastack vs Indie Kit Pro Indie Kit Pro is a feature-rich Next.js starter with credits, lifetime-deal redemption and six payment providers. supastack adds teams, mobile, desktop, API keys and a 144-file test suite. _Published 2026-09-23._ ## TL;DR Indie Kit Pro is built for indie founders who want to monetize fast: credits, a page to redeem lifetime-deal codes, and more payment providers than almost any other starter. supastack is built as a foundation for products that grow into teams, mobile apps and integrations. - Pick supastack for B2B products, teams, mobile or desktop apps, API integrations, and a codebase with real tests. - Pick Indie Kit Pro for a web product sold with credits or lifetime deals, especially if you need Paddle, PayPal or Dodo Payments. ## At a glance | | supastack | Indie Kit Pro | |---|---|---| | Structure | Monorepo: web, mobile, desktop | Single Next.js app | | Web | Next.js 16.3 | Next.js 16.3 | | Mobile / desktop | Expo, Tauri | No | | Auth | Better Auth with 2FA, passkeys, OTP, magic link, admin | Better Auth | | Team workspaces | Yes, invites and roles | No (sold separately as the B2B edition) | | Billing | Stripe or Lemon Squeezy, per-seat | Stripe, Polar, Paddle, Dodo, PayPal, Lemon Squeezy | | Credits | No | Yes, with history and expiry | | Background jobs | Trigger.dev | Inngest | | AI | Router, encrypted BYOK keys, budgets | OpenAI, Replicate helpers | | API keys + outbound webhooks | Yes | No | | Audit log, GDPR export | Yes | No | | Docs / blog / waitlist | CMS + docs | Docs, blog, waitlist | | Test files in repo | 144 | 0 | ## Where supastack is stronger Tests — 144 test files and a local verification gate versus none. When you customize auth or billing, supastack tells you what you broke. Teams in the same codebase — organization workspaces, invites and seat billing are built in. With Indie Kit, teams live in a separate B2B repository. Mobile and desktop — Expo and Tauri apps share the same packages. Integrations and trust features — API keys, signed webhooks, audit log, GDPR export, admin impersonation and bans. AI cost control — per-workspace BYOK keys and usage budgets. ## Where Indie Kit Pro is stronger Monetization options — six payment providers, including Paddle, PayPal and Dodo, which matter if Stripe is not available to you. Credits and lifetime deals — a credits system with expiry and history, plus a built-in flow to redeem lifetime-deal codes, useful for AppSumo-style launches. Launch pages — waitlist, blog and docs are ready for launch day. ## Who should pick which | You are building | Pick | |---|---| | Web tool sold with credits or on AppSumo | Indie Kit Pro | | Product that needs Paddle, PayPal or Dodo | Indie Kit Pro | | B2B SaaS with teams | supastack | | Product with mobile or desktop apps | supastack | | Product customers integrate with via API | supastack | ## Verdict Indie Kit Pro is excellent for getting paid quickly on the web. supastack is the stronger long-term foundation: teams, multiple clients and integrations in one codebase, protected by a test suite. Comparison based on the source code of both starters as of September 2026. Check each project's docs for the latest features. --- Source: [supastack vs Indie Kit B2B](https://supastack.dev/blog/supastack-vs-indie-kit-b2b.md) # supastack vs Indie Kit B2B Both give you organizations, invites and per-team billing. supastack adds mobile, desktop, API keys, webhooks, audit log and 144 test files on Better Auth. A side-by-side comparison. _Published 2026-09-23._ ## TL;DR If you are building a product that companies buy for their teams, both starters give you the core: organizations, invitations and billing per organization. They differ in auth, depth and testing. - Pick supastack for a B2B product that also needs API access, webhooks, audit logs, mobile or desktop apps, and a tested codebase. - Pick Indie Kit B2B if you need per-organization credits or payment providers like Paddle, PayPal or Dodo. ## At a glance | | supastack | Indie Kit B2B | |---|---|---| | Structure | Monorepo: web, mobile, desktop | Single Next.js app | | Web | Next.js 16.3 | Next.js 16.2 | | Auth library | Better Auth (Auth.js optional) | Auth.js (next-auth 5 beta) | | 2FA / passkeys | Yes | No | | Organizations + invites | Yes, owner/admin/member roles | Yes | | Billing | Stripe or Lemon Squeezy, per-seat | Stripe, Polar, Paddle, Dodo, PayPal, Lemon Squeezy | | Credits per organization | No | Yes | | API keys + outbound webhooks | Yes | No | | Audit log, GDPR export | Yes | No | | Admin impersonation and bans | Yes | Basic admin | | Mobile / desktop | Expo, Tauri | No | | Last update in the version reviewed | September 2026 | June 2026 | | Test files in repo | 144 | 0 | ## Where supastack is stronger Enterprise buyers ask for these — 2FA, passkeys, audit logs, GDPR export and API access show up on B2B security questionnaires. supastack ships them. Modern auth — Better Auth by default, with Auth.js as an option. Indie Kit B2B still runs on the next-auth 5 beta. Integrations — workspace API keys and signed webhooks. Clients — mobile and desktop apps for field teams or power users. Tests and freshness — 144 test files and active updates. ## Where Indie Kit B2B is stronger Payment choice — six providers, useful where Stripe is not an option. Credits per organization — good for usage-based B2B pricing. ## Who should pick which | You are building | Pick | |---|---| | B2B SaaS that must pass security reviews | supastack | | Team product with an API or integrations | supastack | | Team product with a mobile app | supastack | | Team product with credit-based usage pricing | Indie Kit B2B | | Team product that needs Paddle, PayPal or Dodo | Indie Kit B2B | ## Verdict Indie Kit B2B covers the basics of team billing with more payment choices. supastack is the more complete B2B foundation: modern auth, security features buyers ask for, integrations and a test suite. Comparison based on the source code of both starters as of September 2026. Check each project's docs for the latest features. --- Source: [supastack vs TanStarter](https://supastack.dev/blog/supastack-vs-tanstarter.md) # supastack vs TanStarter TanStarter runs a lean TanStack Start app on Cloudflare Workers. supastack is a tested Next.js + Expo + Tauri monorepo with teams and integrations. How to choose. _Published 2026-09-23._ ## TL;DR TanStarter bets on TanStack Start and Cloudflare Workers: a lean codebase with very low hosting costs. supastack bets on Next.js, Expo and Tauri in one monorepo, with teams and a developer platform built in. - Pick TanStarter if hosting cost must be near zero, your product is web only, and you like TanStack Start. - Pick supastack if you need mobile or desktop apps, team workspaces, API keys and webhooks, or the Next.js ecosystem. ## At a glance | | supastack | TanStarter | |---|---|---| | Framework | Next.js 16.3 + Expo 57 + Tauri | TanStack Start + Vite | | Hosting | Vercel, Docker, self-host | Cloudflare Workers | | Mobile / desktop | Yes | No | | Auth | Better Auth with 2FA, passkeys, OTP, admin | Better Auth with admin | | Team workspaces | Yes | No | | Billing | Stripe or Lemon Squeezy, per-seat | Stripe, Creem, Waffo | | Credits | No | Basic | | AI | Router, BYOK keys, budgets | AI SDK + fal.ai | | API keys + webhooks | Yes | No | | Storage | Storage package | Cloudflare R2 | | Languages | en, de, es, fr | en, zh | | Test files in repo | 144 | 20 (+ Playwright) | | TypeScript files | ~470 | ~365 | ## Where supastack is stronger Ecosystem — Next.js has far more examples, libraries and AI coding-assistant knowledge than TanStack Start, which means fewer surprises. Multiple clients — mobile and desktop apps share the same packages. B2B features — teams, seat billing, API keys, webhooks, audit log, GDPR export. Tests — 144 test files versus 20. ## Where TanStarter is stronger Hosting cost — Cloudflare Workers and R2 keep the bill very low at small and medium scale. Lean codebase — fewer files and one app to learn. Chinese and English — both included. More payment choices for Asia — Creem and Waffo alongside Stripe. ## Who should pick which | You are building | Pick | |---|---| | Low-traffic web tool on a tight budget | TanStarter | | Web product for Chinese users | TanStarter | | Team or B2B product | supastack | | Product with mobile or desktop apps | supastack | | Product with API integrations | supastack | ## Verdict TanStarter is a smart choice for lean, web-only products where hosting cost dominates. supastack is the better choice once the product needs teams, more clients or integrations, and its test suite makes it safer to grow. Comparison based on the source code of both starters as of September 2026. Check each project's docs for the latest features. --- Source: [supastack vs AnotherWrapper](https://supastack.dev/blog/supastack-vs-anotherwrapper.md) # supastack vs AnotherWrapper AnotherWrapper ships eight ready AI app screens and seven model providers. supastack ships teams, mobile, API keys, BYOK budgets and a 144-file test suite. Which do you need? _Published 2026-09-23._ ## TL;DR AnotherWrapper is a collection of finished AI apps: chat, image studio, video studio, voice, audio, vision and more, wired to many model providers. supastack is the infrastructure underneath an AI product: workspaces, billing, encrypted keys, budgets, jobs and integrations. - Pick AnotherWrapper if you want to launch an AI generation tool fast and most of the value is in the AI screens. - Pick supastack if you are building a durable AI SaaS with teams, customer API access, cost controls and mobile apps. ## At a glance | | supastack | AnotherWrapper | |---|---|---| | Structure | Monorepo: web, mobile, desktop | Single Next.js app | | Web | Next.js 16.3 | Next.js 16.3 | | Mobile / desktop | Expo, Tauri | No | | Ready AI screens | Reference flows | 8 apps: chat, site auditor, resume reviewer, image, video, voice, audio, vision | | Model providers | Router with OpenAI and Anthropic adapters, extendable | OpenAI, Anthropic, Google, Groq, xAI, DeepSeek, Replicate | | Bring-your-own keys | Yes, encrypted per workspace | No | | Usage budgets | Yes, against a price catalog | No | | Credits | No | Yes | | Team workspaces | Yes | No | | Admin panel | Users, orgs, impersonation, bans, orders, flags | Minimal | | API keys + webhooks | Yes | No | | Billing | Stripe or Lemon Squeezy | Stripe, Polar, Lemon Squeezy | | Monitoring | Observability package | Sentry, PostHog | | Test files in repo | 144 | 2 | ## Where supastack is stronger Cost and key safety — each workspace can use its own provider keys, stored encrypted, with usage budgets. That is what stops one customer from running up your AI bill. Teams and admin — organization workspaces, invites, roles and a real admin panel with impersonation and bans. Integrations — API keys and signed webhooks for customers who want to automate against your product. Clients beyond the web — mobile and desktop apps. Tests — 144 test files versus 2. ## Where AnotherWrapper is stronger Speed to a demo — eight AI apps are finished. You can rebrand one and launch in days. Provider breadth — seven model providers are already wired, including image and video generation through Replicate. Credits — ready for pay-per-generation pricing. ## Who should pick which | You are building | Pick | |---|---| | Image, video or voice generation site | AnotherWrapper | | Quick AI tool to test an idea | AnotherWrapper | | AI product for teams, with their own model keys | supastack | | AI API that customers call from their systems | supastack | | AI product with a mobile app | supastack | ## Verdict AnotherWrapper gets an AI tool on screen fastest. As general-purpose AI agents make standalone AI screens easier to copy, the lasting value moves to what supastack provides: teams, cost control, integrations and a tested codebase. Comparison based on the source code of both starters as of September 2026. Check each project's docs for the latest features. --- Source: [Building a SaaS with a coding agent: a practical workflow](https://supastack.dev/blog/building-saas-with-a-coding-agent.md) # Building a SaaS with a coding agent: a practical workflow From a useful brief to a reviewed change. Give your coding agent the context, boundaries, and feedback it needs to build something you can maintain. _Published 2026-09-19._ ## Start with an observable outcome A coding agent can produce a convincing screen before you have decided what the screen should do. Start with a small user outcome: a signed-in user edits their profile, saves it, and sees the saved name after reloading. That gives both the implementation and the review a concrete target. Include the current behavior, the desired behavior, and the important failure cases in the brief. Name the existing module that owns the work. A request to add profile editing should point to the authentication layer, rather than leave the agent to invent a second user store. Keep credentials out of prompts and screenshots — a pasted `apps/web/.env.local` or session cookie becomes part of the conversation record. A workable brief reads like this: “In the account screen, let a signed-in user edit their display name and see it after reload. The auth module owns the user record; do not add a table. An empty name, or a request that names another user's record, must be rejected. Prove it with `pnpm test` and one manual save at a narrow width.” Five sentences, and most of the guessing is gone. - The user-visible outcome, phrased so you can check it in the running app. - The file or module that owns the behavior today — and the areas that should stay untouched. - At least one failure case: an unauthenticated attempt, invalid input, or a repeated request. - The check that proves it — a test command, a fixture, or an explicit manual walkthrough. ## Make the repository explain itself Document how to run the application, which checks are deterministic, and where ownership is enforced. In supastack, `AGENTS.md` records the workspace boundary and `.ai/` records file-level decisions. These notes are most useful when they tell the next contributor — human or agent — why an implementation exists, not just which files changed. The [agent setup guide](https://supastack.dev/docs/web/agent-setup.md) shows how the kit exposes that material, and the [local setup guide](https://supastack.dev/docs/web/local-setup.md) is the walkthrough the agent will actually follow. Ask the agent to read the relevant code before editing. A narrow change usually needs the interface, its server boundary, and the tests around that boundary. Loading the entire repository is less useful than understanding the few places that decide whether the new behavior is correct: the ownership check, the schema, the entitlement. When a feature touches configuration, name the file rather than transcribing it — “add the key to `apps/web/.env.local`” keeps secrets on disk. The same discipline applies to provider dashboards: describe the destination, don't paste the credential. ## Work in small, reviewable changes Agents are most reliable when a change has one owner and one checkable outcome. “Add profile editing” is reviewable; “improve the account area” invites refactors nobody asked for. If a feature decomposes into a schema change, a server boundary, and a screen, run them as separate briefs that each leave the build green. Commit or checkpoint before each agent run. A clean baseline turns a bad output into a deleted diff rather than an archaeology exercise, and it gives your review a stable before-and-after to compare. ## Review one complete change at a time Write an acceptance check before changing behavior. For profile editing, cover a valid update and an unauthorized attempt. Build the smallest implementation that satisfies those cases, then inspect the actual interaction with realistic text and a narrow viewport. Review the diff as well as the result. Look for duplicated business logic, broad exception handling, and new dependencies that solve a problem the existing stack already handles. Run the project's checks and record what they establish. A browser fixture can verify a form interaction; it cannot establish that the production database or an external provider is configured correctly. Each command answers a different question. `pnpm test` exercises behavior against deterministic fake and local adapters, `pnpm check` covers lint, typechecking, and formatting, and `pnpm build` surfaces the integration errors that only appear at bundle time. Together they are a floor for review — the [launch checklist](https://supastack.dev/blog/saas-launch-checklist.md) covers what still has to be proven in the real environment. ``` pnpm test pnpm check pnpm build ``` ## When the agent gets it wrong When a result is wrong, resist stacking follow-up prompts onto a bad diff. Revert to the checkpoint, tighten the brief around whatever was misunderstood — usually an unstated constraint or a missing owner — and run again. Each retry should carry new information, not just new urgency. Persistent failure usually means the brief assumes context the repository does not contain. If the agent keeps inventing a second place to store users, the fix is a sentence in `AGENTS.md` about where identity lives, not a firmer tone. Update the document, then retry the task — the [agents guide](https://supastack.dev/docs/web/agents.md) is where the kit keeps this workflow. ## Common questions about agent workflows A few questions come up whenever a founder starts trusting a coding agent with real product work. - Can a coding agent build a whole SaaS by itself? — It can assemble the visible parts quickly, but the durable parts — authorization, billing lifecycle, data isolation — still need a brief, a check, and a review. Speed without that loop turns into rework. - How big should a task for a coding agent be? — Small enough that you can verify the result in the running app within minutes: one user-visible outcome, one owning module, one acceptance check. - What should never go into a prompt? — Provider secrets, session cookies, the contents of `.env.local`, and customer data. Name files and let the tool read them locally instead of copying values into a chat log. - Does agent-written code change how I review? — The review shifts from typing to reading: inspect the diff for duplicated logic, broad exception handling, and dependencies that re-solve a problem the stack already handles. - Which tests matter most for agent output? — The ones that pin ownership and failure behavior: unauthorized requests, invalid input, and repeated submissions, not only the happy path the demo shows. --- Source: [How to choose a SaaS stack you can keep shipping with](https://supastack.dev/blog/choosing-a-saas-stack.md) # How to choose a SaaS stack you can keep shipping with Frameworks are only part of the decision. Evaluate the whole path from identity and data to background work, deployment, and everyday maintenance. _Published 2026-09-17._ ## Choose around the product's constraints Write down what the product needs before comparing framework names. A web dashboard, an offline mobile tool, and a long-running research workflow have different constraints. Decide which clients you need, what data belongs to each customer, and which operations must continue after a browser tab closes. Consider your team's experience alongside feature lists. A familiar stack with clear boundaries can be easier to maintain than a newer combination that requires learning authentication, deployment, and persistence conventions at the same time. Count the ongoing work of upgrades and debugging, not just the first installation. - Which client surfaces you need on day one — web only, or mobile and desktop too. - What data belongs to each customer, and whether teams share workspaces. - Which operations must outlive a browser tab: queued jobs, scheduled work, long-running tasks. - What you will bill for — subscriptions, seats, usage — and who pays for it. - How much operational surface your team can realistically run: databases, queues, providers. ## Evaluate the layers underneath the framework Framework names describe the surface. The layers underneath decide whether the product holds together. Walk them one at a time and ask the same question of each: who owns this decision, and where is it enforced? Treat any layer a stack leaves blank as work you are signing up for. “Bring your own auth” is a legitimate design — it just means you will own session semantics, provider integrations, and every failure case that follows. Blank layers are a choice; invisible layers are a liability. The kit's [module documentation](https://supastack.dev/docs/modules.md) is a useful reference for what an owned layer actually looks like. | Layer | The question to answer | |---|---| | Identity | Who issues and verifies sessions, on every surface? | | Data | How is each record scoped to its owning customer? | | Billing | Where does a payment event become access, and who revokes it? | | Background work | What happens when a job fails, retries, or runs twice? | | AI and providers | Who owns the keys, the model choice, and the spend limit? | | Operations | How do secrets, logs, backups, and upgrades actually run? | ## Inspect the seams between tools A stack is a collection of integration decisions. Find out who owns sessions, how records are scoped, where a payment event becomes an entitlement, and how the UI retrieves the result of background work. These connections determine whether the happy path remains correct when requests fail or arrive twice. supastack uses Next.js for the web surface, Expo for mobile, and shared TypeScript packages underneath. Better Auth owns identity, Postgres and Drizzle persist records, and Trigger.dev owns explicit workflow behavior. Those are repository choices, not a claim that every product should use the same tools — the [tech stack guide](https://supastack.dev/docs/web/tech-stack.md) records each choice and its reason, and the questions transfer even if you pick different tools. Whatever the tools, the test is the same: pick a record and trace it from the interface, through the server boundary, into persistence, and back. If any hop is a convention rather than an enforced rule, a future contributor will eventually break it. ## Count the cost of keeping it running A stack decision is also an operations decision. Postgres needs backups and migration discipline. A job runner needs a dashboard someone actually opens. Every provider key needs rotation and a home outside client bundles. Add these line items to the comparison before the feature checklist wins you over. Check the upgrade path while you still have options. A monorepo with pinned dependencies and a documented update process — the kit maintains an [update guide](https://supastack.dev/docs/web/updating-the-kit.md) for exactly this — ages better than a stack assembled from tutorials that each assume a different version of the world. - Dependency and security updates: how often they land, and who applies them. - Provider credentials: where they live, and who can rotate them. - Data: a backup schedule, a restore drill, and a retention policy. - Observability: what breaks loudly, and what fails silently until a customer reports it. ## Build a small vertical slice Before committing to a stack, run a slice that signs a user in, writes an owned record, starts work, and retrieves a result. Then deliberately try the wrong workspace, an expired session, and a failed job. Evaluate how much custom glue those ordinary conditions require. Keep a short decision record: what the stack supports, what you are choosing to postpone, and what would trigger a revisit. Native purchasing, real-provider acceptance, and deployment operations should be explicit items rather than assumptions hidden behind a successful local build. The right stack leaves the team enough attention to work on the product. If the slice goes well inside a starter's repository, the follow-up question is whether to adopt it — [starter kit or from scratch](https://supastack.dev/blog/starter-kit-or-from-scratch.md) walks through that decision. - Sign in, then restore the session after a reload. - Write a record scoped to one workspace and read it back. - Start a background task and retrieve its result later. - Repeat the same calls from the wrong workspace and an expired session. ## Common questions about choosing a stack Stack decisions attract a lot of confident advice. These are the questions worth answering for your own product instead. - Should I pick the stack I already know? — Usually yes, when it can express the product's constraints. A familiar stack with clear boundaries beats a fashionable one you will spend months learning — unless the product genuinely requires something it lacks, like native mobile. - Does the front-end framework matter most? — Less than the layers around it. The framework shapes the UI; identity, scoping, billing, and background work decide whether the product stays correct. - How long should a stack evaluation take? — Days, not weeks: write the constraints, inspect the seams, and run one vertical slice. If ordinary failures need custom glue, keep looking. - When is switching stacks later justified? — When a core constraint changes — a required platform, a scale threshold — not when a newer release looks nicer. The switching cost lives in auth and data, not in components. --- Source: [Starter kit or from scratch? Decide what you want to own](https://supastack.dev/blog/starter-kit-or-from-scratch.md) # Starter kit or from scratch? Decide what you want to own A practical way to evaluate a boilerplate: inspect its boundaries, run its recovery paths, and understand the maintenance you are taking on. _Published 2026-09-16._ ## Separate product work from repeated infrastructure List the features that make your product useful, then list the infrastructure it needs to operate. A research workflow may be distinctive; session handling, customer-owned records, and subscription access usually are not. This inventory gives you a better evaluation tool than counting the number of features on a starter's homepage. The second list is longer than it looks. Most SaaS products re-implement the same dozen pieces, and each one has to integrate correctly with the others before the product can do anything at all. Building from scratch gives you direct control over those choices, but also responsibility for integrating and maintaining them. A starter changes the starting point. You still need to understand its code and verify the assumptions it makes about your product, team, and deployment. - Identity: sign-in methods, sessions, account recovery. - Workspaces: record ownership, invitations, member roles. - Billing: checkout, webhooks, entitlements, cancellation. - Background work: queues, retries, schedules, status tracking. - Delivery: transactional email, file storage, deployment. ## Compare total cost, not the sticker price A starter's price is visible; the from-scratch cost is distributed across months. Tally it honestly: the weeks spent assembling auth, billing, jobs, and deployment; the integration bugs those assemblies ship with; and the maintenance each provider update triggers afterward. The starter's cost is learning and adapting code you did not write: reading its conventions, replacing sample content, occasionally bending a boundary it drew. When the inspection below goes well, that trade is usually cheaper than rebuilding the same plumbing. When it goes badly, it is a warning you should not ignore — an unreadable boundary today is a production incident next year. ## Evaluate the code you will inherit Run the setup instructions and follow one record from the interface to persistence. Check whether authorization lives at the server boundary, whether configuration is validated, and whether provider credentials stay outside client bundles. Read a negative test, not just the demo screen. Look for places where the example product has become inseparable from the infrastructure. If removing a demo feature requires changing authentication or billing behavior, future product work may be harder than the initial setup suggests. In supastack, consuming products keep their domain logic outside Starter Core and connect through configuration and contracts — the [project structure guide](https://supastack.dev/docs/web/project-structure.md) shows where that line sits. - Every undocumented setup step you had to figure out yourself. - Where authorization is enforced, and whether it is per-route or systematic. - Whether a negative test exists — a cross-workspace read, a replayed webhook — and what it proves. - Where secrets live, and whether any reach a client bundle. - What happens to the infrastructure when you try to delete a demo feature. ## Run it before you decide Spend a day inside the repository before committing. In supastack that means `corepack pnpm install`, a configured `apps/web/.env.local`, and the local reference flow: sign in, inspect access, submit a task, read its result. Then `pnpm verify:local` runs the golden path against fake providers with no credentials required — the [local setup guide](https://supastack.dev/docs/web/local-setup.md) walks through the sequence. Then break something on purpose. Submit the same task twice. Call an API route with the wrong workspace. Replay a webhook. A starter that handles ordinary failure gracefully is worth more than one with a longer feature list, because what you cannot make fail cleanly on your machine you will eventually debug in production. ## Make the tradeoff explicit Choose a starter when its existing boundaries match your needs and you can explain the flow you will depend on. Choose a custom foundation when the product's constraints would force repeated changes to the starter's assumptions. Either choice can be reasonable; the important question is which maintenance burden your team can carry. Reserve time to replace sample content, configure integrations, review data ownership, and test deployment. A repository that compiles is a starting point for production acceptance. Before launching, verify the real sign-in path, billing lifecycle, and recovery behavior in the environment your customers will use — the [launch checklist](https://supastack.dev/blog/saas-launch-checklist.md) enumerates it. And if the underlying stack is also undecided, [choosing a SaaS stack](https://supastack.dev/blog/choosing-a-saas-stack.md) covers the layer-by-layer evaluation; the two decisions inform each other. | | Build from scratch | Adopt a starter kit | |---|---|---| | You own | Every convention, integration, and failure mode | The product code; the plumbing comes maintained | | Early speed | Slow — plumbing comes first | Fast — features start on day one | | Risk sits in | Seams you have not found yet | Boundaries you have not inspected yet | | Right choice when | Your constraints break typical assumptions | Its boundaries match your product's needs | ## Common questions about starter kits The buy-versus-build debate generates strong opinions. These are the questions that actually move the decision. - Is using a starter kit risky for a serious product? — Plenty of production SaaS runs on bought foundations. The risk is not the starter; it is inheriting code you have not inspected. Read the boundaries before you depend on them. - How do I tell if a boilerplate is maintained? — Look for recent commits, a documented update path, tests that exercise failure cases, and issues answered with specifics rather than roadmap promises. - What if the starter's choices don't match mine? — Distinguish preferences from constraints. Re-skinning a UI is cheap; fighting a session model or a tenant boundary is not. A structural mismatch is a reason to build your own. - Can I move off a starter later? — You own the code, so there is nothing to migrate away from — the real question is whether its plumbing stays an asset as you modify it, which is why separable product and infrastructure code matters. - How much time does a starter actually save? — Roughly the weeks that auth, billing, jobs, and deployment would have taken, minus the days needed to learn and adapt what you inherited. --- Source: [A SaaS launch checklist that goes beyond the homepage](https://supastack.dev/blog/saas-launch-checklist.md) # A SaaS launch checklist that goes beyond the homepage Verify sign-in, customer data, paid access, failed tasks, and support paths before inviting people into your product. _Published 2026-09-14._ ## Walk through the customer's first session Create a fresh account in the environment you plan to release. Follow verification or provider callbacks, reload the application, update a profile, and sign out. Repeat the journey on a narrow screen using a keyboard. Your normal development session can hide missing setup steps and stale state. Check the messages that explain what to do next. An empty screen should offer a useful action. A failed request should explain how to retry without implying that work succeeded. Use the actual product's language in those messages, and make sure account and support links reach real destinations. Repeat the journey once per sign-in method. OAuth callbacks for Google and GitHub each carry their own redirect and failure modes (the [OAuth setup guide](https://supastack.dev/docs/web/setup-oauth.md) covers configuring them), and a provider's test consent screen behaves differently from a production one. - Fresh account creation and session restoration work. - Sign-out revokes access and clears the previous user's presentation. - Empty, loading, and error states remain understandable on mobile. - Every OAuth provider completes its callback on the real domain. - Verification and reset emails arrive, read clearly, and link to the right environment. - The first-run experience tells a new user what to do next. ## Prove paid access at the server Checkout is the easy half. Walk the whole lifecycle in the provider's sandbox: purchase, webhook received and verified, entitlement persisted, paid feature unlocked — then renewal, cancellation, and refund. Every transition should change server-owned state, not just a client flag. The [billing guide](https://supastack.dev/docs/web/billing.md) shows where supastack keeps that state. Then test the events you hope not to see: a webhook delivered twice, a failed payment, a subscription lapsing in the middle of a task. An entitlement answers “is this allowed right now” at request time; a dashboard badge does not. - A sandbox purchase completes and unlocks the paid feature. - Duplicate webhook deliveries are safe — processing is idempotent. - Cancellation and refund revoke access at the server boundary. - Per-seat or usage limits, if offered, reject beyond their terms. ## Verify the boundaries with real dependencies Use two independent accounts to verify ownership. Try retrieving and modifying one account's record from the other account, including task status and recovery URLs. Check paid access at the server action boundary, not only by hiding a button in the interface. Workspace-scoped records are the spine of a multi-tenant product — the [workspace boundaries post](https://supastack.dev/blog/workspace-boundaries.md) explains the model. Whatever your stack, the launch question is the same: does every read and write derive ownership from the server's authenticated context, with no record ID or client-sent workspace accepted as proof? - Cross-workspace reads and writes are rejected. - Recovery and task URLs do not grant access on their own. - List and detail views enforce the same scoping. - API keys and webhooks, if offered, carry workspace scope too. ## Exercise the failure paths on purpose Background work fails in two distinct ways, and each needs its own test. A failed dispatch leaves the client unsure whether anything was queued — a task-scoped idempotency key and a saved submission let recovery reuse the original request instead of duplicating it. A failed completion means the work ran but the result never arrived — retries and status polling should close that gap without a second generation. Apply the same thinking to providers: an AI provider timeout, a bounced email, a billing API error. Each should end in a specific, explainable state rather than a silent half-result. The [task recovery guide](https://supastack.dev/docs/modules/task-recovery.md) documents how the kit's production path handles this. - A retry after a submission timeout does not duplicate work or cost. - A failed completion is visible and recoverable, not a silent loss. - Provider errors reach the user as honest states, not generic crashes. ## Prepare for the first failure Make sure someone can find an error, connect it to the relevant task, and help the user recover without exposing provider secrets. Confirm which logs and audit records exist, how long you retain them, and how an operator gets access to them. Decide what “something is wrong” looks like before launch: failed job deliveries, webhook verification errors, a sudden climb in provider spend. An alert nobody receives is not monitoring — the [observability setup](https://supastack.dev/docs/web/setup-observability.md) covers wiring those signals. Treat backups as an operational exercise: a configured backup is not the same as a restored one. Run the restore once, on data that resembles production, before the day you need it. - An error can be traced to the request, user, and task that caused it. - Logs and audit records have an owner, a retention policy, and access rules. - A backup has been restored, not just configured. - Failed jobs and webhook errors raise an alert a person receives. ## Check the surfaces around the product Legal and communication surfaces are part of launch, not polish: privacy and terms that describe what the product actually does, a support address that reaches a person, and a verified sender domain so transactional mail arrives from an address you own. Read every automated email as a customer would. Verification, reset, receipt, and completion messages should carry the product's name, links to the right environment, and a reply path that is not a dead `noreply` address. - Privacy and terms pages describe the product you actually run. - Transactional mail sends from a verified domain you own. - Support and billing questions have a reachable destination. ## Make the go or no-go decision explicit Assemble the release record: the build revision, the target environment, what was tested and how, and what remains unverified. A checklist with honest gaps beats a confident summary — the gaps are where the first incident usually lives. Decide the rollback path before you need it: how to revert the deployment, what happens to data written in between, and who makes the call. Then launch the smallest version of the offer you can fully support, and widen it deliberately. ## Common launch questions Launch checklists fail in predictable places. These are the questions that catch most of them. - What do most SaaS launches forget? — The ends of flows: sign-out that actually revokes, cancellation that actually removes access, and a restore drill for the backup that exists on paper. - How many test accounts do I need? — At least two, in the target environment. Data isolation and invitation flows cannot be verified with one. - Sandbox testing or production testing? — Both. Sandbox proves the lifecycle logic; a small real transaction proves keys, domains, and webhooks are wired to the right place. - When is it safe to launch? — When every item protecting customer data or money is verified, and the unverified items are written down with owners — not when the feature list looks complete. - What changes after launch? — Keep the release record, watch the failure alerts you configured, and re-run the checklist after any significant change to auth, billing, or scoping. --- Source: [SaaS authentication is more than a login form](https://supastack.dev/blog/authentication-beyond-the-login-form.md) # SaaS authentication is more than a login form Follow identity through sessions, workspace ownership, protected actions, and sign-out — the parts a polished sign-in screen cannot prove. _Published 2026-09-13._ ## Let one system own identity The login form collects a request. The authentication system decides whether that request establishes an identity and issues the session the rest of the application trusts. Keeping that responsibility in one place prevents each route from inventing its own interpretation of a signed-in user. supastack composes Better Auth for identity and session handling. Application code resolves the session at protected boundaries and connects it to a workspace. The interface can show helpful access states, but client state must not become evidence that an operation is authorized. The rule has to hold on every surface the product ships. Web, mobile, and desktop clients should consume the same identity decision rather than implementing it three times — three interpretations of “signed in” is where account bugs come from. ## Choose sign-in methods deliberately Every method trades friction against attack surface and an outside dependency. Passwords need reset flows and rate limiting. Magic links and one-time codes depend on email deliverability. OAuth delegates identity to providers like [Google](https://supastack.dev/docs/web/google-sign-in.md), [GitHub](https://supastack.dev/docs/web/github-sign-in.md), or [Apple](https://supastack.dev/docs/web/apple-sign-in.md) — and inherits their outages, consent screens, and callback rules. supastack composes these through Better Auth — email OTP, password, magic link, OAuth, with two-factor and passkeys available — behind a single identity decision. Ship the smallest set your audience actually needs; each additional method is another callback path, another failure mode, and another flow to verify before launch. The [authentication guide](https://supastack.dev/docs/web/authentication.md) covers the wiring. When a person can sign in more than one way, decide how the methods join. A Google account and an email magic link that resolve to the same address should land on one identity, not two. Account-linking rules are part of the identity decision, and getting them wrong creates duplicate users that surface months later as mysteriously missing data. ## Sessions are the credential that matters After sign-in, the session — not the password — is what every request carries. Its expiry, storage, and invalidation rules define the real security posture. A session that survives sign-out, or one a removed workspace member can keep using, is an authentication failure no polished login screen can fix. Decide the lifecycle up front: how long sessions live, whether users can see and revoke their active sessions, and what a password change, email change, or workspace removal does to them. Session management exists as a module in the kit precisely because these decisions are easy to postpone and expensive to retrofit. ## Connect the session to the requested record Being signed in does not grant access to every record. Resolve the user's workspace on the server and include that scope in reads and mutations. A task identifier or recovery link is a reference to work, not permission to retrieve it — the [workspace boundaries post](https://supastack.dev/blog/workspace-boundaries.md) covers that side of the boundary in detail. Resolve the scope in a fixed order: authenticate the session, load the membership, then read or write with that workspace context — never the other way around. Middleware can redirect an anonymous visitor, but it cannot authorize the operation; the check belongs in the server action or route handler where the record is touched. ## Accounts have a lifecycle, not just a beginning Provisioning is the first test of that boundary. When a user signs up, the workspace and ownership records that follow must be safe to create more than once — lifecycle hooks can be replayed by providers, retries, or support tools. A duplicate event should be a no-op, not a duplicate organization. The end of the lifecycle deserves the same care. Email changes, password resets, member removal, account deletion, and data export are each small flows with their own abuse cases — a reset link that works twice, an export that leaks a teammate's records. supastack ships GDPR export and session management for this reason; whatever you build on, budget for the boring flows. ## Test the lifecycle, including its end A successful login test is useful, but incomplete. Exercise invalid credentials, expired or reused verification codes, anonymous requests, and cross-workspace access. Verify that sign-out invalidates the session and that a returning client cannot keep using a previously authorized screen to perform protected work. Distinguish handler tests from deployment acceptance. A memory adapter can establish the semantics of a real authentication handler without proving Postgres persistence or a provider callback. Verify the configured origin, callback path, session restoration, and sign-out with the deployed dependencies before claiming that the complete sign-in journey works. - Invalid credentials and expired or reused verification codes are rejected. - Anonymous and cross-workspace requests receive no owned data. - Sign-out invalidates the session — a stale client cannot keep acting. - Provisioned records stay idempotent when lifecycle hooks replay. - Real-provider callbacks work on the configured origin — verified, not assumed. ## Common authentication questions The questions below surface once the login form is done and the system has to keep working. - Is middleware enough to protect routes? — Middleware is a convenient redirect, not an authorization layer. Each server action and route resolves the session and workspace itself; page-level guards hide screens without authorizing operations. - Build auth myself or use a library? — Session semantics are rarely where a SaaS differentiates. A maintained library like Better Auth behind clear module boundaries gives you tested primitives while keeping the decisions that matter — scoping, lifecycle — in your code. - How do sessions relate to workspaces? — The session identifies the person; the workspace identifies the records they may touch. Resolve membership on the server for every request and never trust a workspace ID the client sends. - What do B2B customers expect from authentication? — Team workspaces with roles, OAuth sign-in, session visibility and revocation, and an audit trail. Two-factor and passkeys are increasingly expected for admin roles. - What breaks authentication in production? — Usually the edges: a callback URL that only exists locally, a hook that provisions twice, a session that outlives a membership. Test the ends of flows, not just the starts. --- Source: [A foundation you can build on.](https://supastack.dev/blog/a-foundation-for-your-next-product.md) # A foundation you can build on. The thinking behind supastack: shared infrastructure, clear boundaries, and room for your product to be its own thing. _Published 2026-09-11._ ## The work every product repeats A new product starts with a particular problem. Its first weeks often look much less particular: sessions, workspace records, subscription state, background work, and email delivery. These pieces need to cooperate before the product can do anything useful — each is a solved problem in the abstract and an integration risk in the specific. supastack gives that repeated work a home. A Next.js web app and an Expo mobile app share TypeScript packages for the infrastructure underneath. The aim is to make those boundaries understandable enough that a product can configure them without inheriting somebody else’s domain model. None of the pieces is hard in isolation. The cost lives in their coordination — a payment that must become an entitlement, a session that must become a workspace scope, a task that must survive a closed laptop. Products rarely fail on a missing feature. They fail on a seam. ## Why build it once, properly The argument for shared infrastructure is not that the work is beneath you — it is that the repeated work deserves to be done once, carefully. A workspace boundary implemented well in a shared module protects every product built on it. The same boundary improvised per product becomes a set of near-misses that differ in interesting, dangerous ways. It also concentrates maintenance. Provider APIs change, auth libraries evolve, billing semantics drift. When those changes land in one place — the kit’s [module documentation](https://supastack.dev/docs/modules.md) tracks them — a fix propagates instead of being re-learned per project. And when you are weighing the trade for a single product, [starter kit or from scratch](https://supastack.dev/blog/starter-kit-or-from-scratch.md) applies the same reasoning to the buy-versus-build decision. Shared infrastructure concentrates review, too. A boundary that lives in one place can be tested hard once — negative cases, replayed events, cross-tenant attempts — instead of hoping five re-implementations each got it right. That is part of why the kit invests in its test suite: maintaining confidence in shared code is cheaper than re-earning it per project. ## Give each module one responsibility Identity establishes who is making a request. Workspaces determine which records that person can access. Billing records payment state, while entitlements answer whether a capability is available. Keeping these concerns distinct prevents a successful payment redirect from becoming an authorization decision. The same discipline applies to background work. The shared JobRunner contract promises enqueue, status, and cancel. Trigger.dev owns the workflow behavior. A small interface is valuable when its limits are visible; hiding retries or scheduling behind a generic method would make the system harder to reason about. Better Auth answers identity. Postgres and Drizzle hold the records. Stripe or Lemon Squeezy report payment state. None of them is asked to be clever about another module’s job — the seams stay narrow enough to reason about — and a foundation is only as trustworthy as its seams. ## Leave room for the product A prospecting workflow, a document editor, and a research assistant may use the same identity and billing modules. Their records, language, and workflows are still different. Product-specific logic belongs in the consuming product codebase, with configuration connecting it to the foundation. This separation is also a practical maintenance decision. A fix to workspace isolation should be reusable. A change to how one product scores a prospect should not become a new requirement for every other product. That is the difference between a foundation and a theme. A theme gives you a look to keep or replace. A foundation gives you load-bearing decisions — who owns identity, how records are scoped, where a payment becomes access — that a product builds on rather than works around. The [project structure guide](https://supastack.dev/docs/web/project-structure.md) shows where one ends and the other begins. ## Start with one complete flow Begin with the local reference flow: sign in, inspect access, submit a task, and read its result. Fake and local adapters let deterministic tests exercise the path without paid provider requests — the [five-minute quickstart](https://supastack.dev/docs/web/ship-in-5-minutes.md) runs it end to end. Then configure the production integrations your product actually needs. Local acceptance and production acceptance answer different questions. A passing local test establishes application behavior under controlled conditions. Real authentication persistence, provider delivery, deployment, and device behavior still need their own verification. Keep that distinction visible as the product grows. A foundation earns its name slowly: in the second product that ships faster than the first, and in the maintenance day that fixes one module instead of five codebases. That is the bet behind supastack — and it is the bar each module is held to. ## Questions behind the approach A few questions sit underneath this approach, and they are worth answering plainly. - Is supastack a template or a framework? — A foundation of modules with contracts between them. Product code lives outside Starter Core and connects through configuration, so the platform can be updated without forking your features. - Won’t shared infrastructure make products generic? — Only the plumbing is shared. Records, workflows, and language stay in the product codebase — the parts customers can actually see remain yours. - Where does the product’s own code begin? — With the records and workflows only it has. The foundation authenticates the user and scopes the data; it does not know what a prospect is. - Why a monorepo? — So the web, mobile, and desktop apps change together with the packages underneath them: one type system, one test gate, no drift between repositories. - What if I only need the web app? — The foundation is the purchase; mobile and desktop are surfaces to adopt or ignore. Nothing forces you to ship an app you do not need. --- Source: [Your workspace is a security boundary.](https://supastack.dev/blog/workspace-boundaries.md) # Your workspace is a security boundary. Why ownership checks belong in every read and write, and why a task URL should never grant access. _Published 2026-09-10._ ## Derive ownership on the server A record ID identifies something; it does not prove who may read it. supastack scopes user-owned records by workspaceId. The server resolves the authenticated user’s workspace and includes that scope when reading or changing a record. Do not treat a workspace ID sent by the browser as evidence of ownership. The browser can request an action, but the trusted server context must establish whose action it is. This rule needs to hold for task status, saved provider configuration, usage records, and every product-owned resource. Layouts and components can filter what renders, but rendering is not authorization — the check belongs where the record is actually read or written. Everything else in this post follows from that rule: ownership is derived, never requested. The moment a client-sent `workspaceId` is trusted anywhere, every other check becomes decoration. The [authentication post](https://supastack.dev/blog/authentication-beyond-the-login-form.md) covers how the session itself is established upstream of this boundary. ## Membership is part of the boundary Scoping answers which workspace a record belongs to; membership answers whether this user belongs to that workspace. Resolve both on the server. supastack models personal and organization workspaces with owner, admin, and member roles — the [workspaces guide](https://supastack.dev/docs/modules/workspaces.md) documents the model. Roles add a second axis: a member may read what only an admin may change. Keep the two checks distinct — “belongs to this workspace” and “may perform this action” — so a demotion or removal takes effect at the same boundary as everything else. Invitations and removals are part of the same lifecycle, not an admin afterthought. Membership changes also have to take effect immediately. A removed member’s next request should fail at the boundary, not whenever a cached token happens to expire — which is another reason the check lives where records are read, rather than inside a credential issued earlier. ## Make recovery useful without granting access A task URL lets a user return after a refresh. It is useful precisely because it preserves a reference to the work. The authenticated API must still verify that the requested task belongs to the current workspace before returning status or results. The same principle applies to submission recovery. An idempotency key helps identify a repeated request and prevent duplicate work. It is neither a session nor an access token. Retrying a saved submission still requires authentication, workspace ownership, and the appropriate entitlement. Convenience paths are where isolation quietly erodes. A link that helps a legitimate user resume work — after a refresh, from a completion email — must pass through the same authenticated check as any other request. The [request-to-result pipeline](https://supastack.dev/blog/from-ai-request-to-result.md) uses task-scoped idempotency keys on exactly this principle. ## Test the requests that must fail A test that lets an owner retrieve a record proves the happy path. It does not prove isolation. Pair it with a second workspace requesting the same record, an anonymous request, and a write that attempts to substitute another owner. Assert the external behavior as well as the persisted result. A rejected request must not mutate another workspace’s data, and an error response must not leak secrets or raw database details. These checks make the boundary reviewable when implementation changes. - A second workspace requests the same record ID — rejected. - An anonymous request hits the same endpoint — rejected. - A member removed from the workspace retries — rejected. - A write names a different owner or workspace — rejected, with nothing mutated. - Error responses stay generic — no secrets, no schema details. ## Carry the rule into your product When you add a new product table, decide its ownership before building the screen. Include workspaceId in its persistence model, resolve the workspace through the existing server boundary, and use that context in both list and detail operations. Background workers need the same care. A queued task must carry enough trusted context to load the correct record, while provider credentials stay on the server. A scheduled job loading rows by bare ID is still inside the boundary. A consistent ownership rule is easier to maintain than a collection of route-specific exceptions. The API surface belongs to the same boundary. Workspace API keys and signed outbound webhooks carry the workspace’s scope with them — a key must never become a way to read across tenants, and a webhook payload must never carry a neighbor’s records. The [API module guide](https://supastack.dev/docs/modules/api.md) covers the shape of that surface. ## Common questions about workspace isolation Multi-tenant isolation raises the same handful of questions on every project. These are the ones worth settling early. - Is application-level scoping enough, or do I need row-level security? — Server-enforced scoping on every read and write covers the threat model for most SaaS. Database RLS adds defense in depth but does not replace boundary checks — workers and API keys still need the same rule. - How should sharing across workspaces work? — Model it explicitly — an invitation, a membership row — rather than weakening the scope. “Anyone with the link” should be a designed feature with its own checks, not an accident. - What breaks first in multi-tenant apps? — Convenience paths: recovery links, admin tooling, background workers loading records by bare ID. Audit every code path that touches a record without going through the normal scope. - How do I add scoping to an existing table? — Add `workspaceId`, backfill it, enforce it in reads and writes, then touch the UI. Ship the negative tests first so the boundary is proven rather than assumed. - Do server actions need checks if the page is protected? — Yes. Page guards hide screens; they do not authorize operations. Every action resolves the session and workspace independently. --- Source: [An AI request is only the beginning.](https://supastack.dev/blog/from-ai-request-to-result.md) # An AI request is only the beginning. Follow a task through authorization, durable execution, usage tracking, and a result the user can return to. _Published 2026-09-08._ ## Before the model runs The user supplies a prompt and an alias such as cheap, fast, or smart. The production server checks authentication, workspace ownership, and entitlement before accepting the task. Provider model IDs and secrets remain behind that boundary — the friendly alias is the only model vocabulary the client ever needs, because the mapping to a real provider model is configuration, not contract. A durable task record gives the request an identity before the background execution begins. That identity connects the submission, the worker, usage records, and the result. It also gives the user a way to recover the work after navigating away. The ordering matters: expensive work starts only after cheap checks pass. Verifying authentication, ownership, and entitlement before anything is enqueued keeps unauthenticated or unpaid requests from ever reaching a provider — and makes the failure explainable to the user. The [AI module guide](https://supastack.dev/docs/modules/ai.md) documents where those checks live; the [workspace boundaries post](https://supastack.dev/blog/workspace-boundaries.md) explains why ownership is derived on the server rather than trusted from the client. ## Decide whose keys and whose limits An AI feature is a spending decision. supastack lets workspaces bring their own provider keys — stored encrypted, never exposed to clients — or run on platform keys, and enforces per-workspace budgets against a model price catalog. Whatever your implementation, the questions are the same: who pays, and where is the limit enforced? Enforcement belongs in the server workflow, not in a browser-side estimate. A budget checked after generation only records overspending; checked before dispatch, it prevents it. [Provider key setup](https://supastack.dev/docs/web/provider-keys.md) covers the configuration side. ## Make execution explicit Trigger.dev runs the ai.generate workflow. The capability-aware router selects a model using configured policy and the request’s requirements. Treat that policy as an operational input: an unavailable alias should not appear as a working choice in the interface. A network failure during submission can leave the client uncertain about whether work was queued. The production path uses a task-scoped idempotency key and a saved submission so recovery can reuse the original request. Sending a new task after every timeout would risk duplicate work and duplicate cost. Durability is also what keeps the interface simple: the screen reads task status and renders it rather than orchestrating retries. When generation spans model calls, tool steps, or schedules, the explicit workflow contract — enqueue, status, cancel — is what keeps “in progress” honest. ## Name the failure modes Between request and result there is a specific, finite set of things that go wrong. Design for each by name rather than for “errors” in the abstract — every task should end in an explainable state: queued, running, failed with a reason, or complete with a result. [Task recovery](https://supastack.dev/docs/modules/task-recovery.md) is the kit’s reference for the recovery side. The interface requirement underneath is that no state can lie. A spinner that outlives the work, or a success message for a task that never ran, is a support ticket and a billing dispute waiting to happen. - Submission timed out — recover through the saved submission and the task-scoped idempotency key instead of re-queuing. - Provider failed mid-task — mark the task failed with a reason the interface can show. - Budget exhausted — reject before dispatch, not after the spend. - User navigated away — the durable record and recovery URL restore status and result. ## Track what happened Generation produces more than text. The application records usage and a task result, and the interface reads status through the authenticated API. The usage record is what makes the budget enforceable — it is written before the result is returned, not estimated after. Completion email is a separate step with separate delivery records. A successful model response does not establish that an email was delivered. Keeping those outcomes distinct makes failures easier to explain and retry without rerunning the model unnecessarily. The result also needs a home the user can return to: a task page, a history list, a recovery URL delivered in that completion email. A generation that exists only in a transient notification is work you will be asked to produce twice. ## Verify each boundary Use deterministic fake adapters to test the application’s decisions first: denied access, disabled aliases, duplicate submissions, result recovery, and generic errors. Those tests should run without provider credentials. Then use the explicitly opted-in provider smoke process for real integrations. It requires the documented sandbox configuration and approval to make external calls. The distinction keeps ordinary development fast while making production evidence specific and accountable. Keep that split stable as the product changes: deterministic coverage near every boundary decision, so a provider swap, a router policy change, or a new alias fails a test rather than failing a customer. ## Common questions about AI features in SaaS A few questions come up in every conversation about putting AI behind a paywall. - Where should authorization for AI features happen? — Before anything is enqueued: authenticate, resolve the workspace, check the entitlement, then create the task. Checks run after generation only document misuse. - Do I need bring-your-own-key support? — If your customers have their own provider relationships, yes: it decides who pays and who owns rate limits. Store keys encrypted and keep them off clients — the [AI configuration guide](https://supastack.dev/docs/modules/ai-configuration.md) covers the model. - How do I keep retries from double-charging? — Scope the idempotency key to the task and reuse the saved submission during recovery, so a timeout cannot create a second billable run. - What usage data should I record? — Per task: model, units consumed, cost against the price catalog, and the workspace — enough to enforce budgets and answer spend questions without rerunning jobs. - When does generation belong in a background job? — Whenever it can outlive a request. Model latency, retries, and multi-step work argue for durable execution; only instant, cheap calls belong inline.