# 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 packages/web-core/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 packages/web-core/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 packages/web-core/src/ Shared pages, 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 `packages/web-core/src/pages/` for its load and view sources — 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 `packages/web-core/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 packages/web-core/src/reports.ts .ai/apps/web/app/reports/page.md .ai/packages/web-core/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/packages/web-core/src/reports.md` explains the decisions behind `packages/web-core/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 `packages/web-core/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 (or saved as `DATABASE_URL_TRANSACTION`). 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 (with optional MARKETPLACE_PLATFORM_FEE_PERCENT for marketplace application fees); 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-email.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 packages/web-core/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 packages/web-core/src/job-handlers.ts and packages/web-core/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 `packages/web-core/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-email.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 `packages/web-core/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 packages/web-core/src/job-handlers.ts and packages/web-core/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 `packages/web-core/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 `packages/web-core/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 `packages/web-core/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 packages/web-core/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 packages/web-core/src/theme/catalog.ts. Extend density, radius, font, and layout axes in packages/web-core/src/theme/presets.ts plus matching html[data-*] rules in app-theme.css. Pick the marketing identity in packages/web-core/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 -- packages/web-core/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 packages/web-core/src/theme/marketing-theme.ts. If text becomes hard to read, restore the previous value and run corepack pnpm test -- packages/web-core/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 packages/web-core/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 -- packages/web-core/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 packages/web-core/src/resources/content.ts. If the table of contents jumps nowhere, give every section a unique id. Run corepack pnpm test -- packages/web-core/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. packages/web-core/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 in 5 minutes](https://supastack.dev/docs/tanstack/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 TanStack 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/tanstack/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/tanstack/terminal-basics.md) [Or let an agent run the setup for you](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack/.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-tanstack/.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-tanstack/.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 TanStack web 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 TanStack web repository, corepack pnpm dev starts the available development workspaces together; in the All Platforms monorepo the unfiltered command leaves this app out, so keep using the filtered command below. 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-tanstack 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/tanstack/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-tanstack/.env.local, then restart. To customize the name and marketing text, open packages/web-core/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/tanstack/troubleshooting.md) [Change your product's name and copy](https://supastack.dev/docs/tanstack/marketing-site.md) --- Source: [Before you start](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/agent-setup.md) [Run the Web app](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack 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-tanstack/.env.local for the TanStack Start Web server, and apps/mobile/.env.local for Expo. TanStack Start 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-tanstack/.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/tanstack/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-tanstack 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/tanstack/troubleshooting.md) --- Source: [Let an agent set it up](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/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/tanstack/ship-in-5-minutes.md) for the first demo, then [Web local setup](https://supastack.dev/docs/tanstack/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/tanstack/agents.md) covers everyday tasks such as new screens, restyling and reviews. --- Source: [Introduction](https://supastack.dev/docs/tanstack.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/tanstack/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/tanstack/terminal-basics.md) [Follow the complete Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ## Build from a shared foundation supastack is a modular foundation for AI SaaS on web, mobile and desktop. TanStack Start 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-tanstack/ TanStack Start 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/tanstack/ship-in-5-minutes.md) [Browse all documentation](https://supastack.dev/docs/tanstack/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/tanstack/using-the-docs.md) ## Welcome to supastack supastack is a modular kit for AI SaaS products: a TanStack Start 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 --filter @supastack/web-tanstack 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-tanstack/.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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack dev ``` ## Application stack The web app is a TanStack Start 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. ## TanStack Start + React The web app runs [TanStack Start](https://tanstack.com/start) on [TanStack Router](https://tanstack.com/router) file routes and [React](https://react.dev) 19 server components. - [TanStack Start](https://tanstack.com/start) - [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:tanstack # browser flows ``` --- Source: [Local setup](https://supastack.dev/docs/tanstack/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 TanStack 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/tanstack/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack/.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-tanstack/.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-tanstack/.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 TanStack web 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 TanStack web repository, corepack pnpm dev starts the available development workspaces together; in the All Platforms monorepo the unfiltered command leaves this app out, so keep using the filtered command below. 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-tanstack 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/tanstack/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-tanstack/.env.local, then restart. To customize the name and marketing text, open packages/web-core/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/tanstack/troubleshooting.md) [Change your product's name and copy](https://supastack.dev/docs/tanstack/marketing-site.md) --- Source: [Project structure](https://supastack.dev/docs/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack 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-tanstack dev. 3. Open http://localhost:3000. Expected: the Web app starts from apps/web-tanstack 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 TanStack Start in apps/web-tanstack, 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-tanstack/src/ File routes, actions and framework wiring packages/web-core/src/ Shared pages, 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-tanstack 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 TanStack 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 TanStack web repository. [Build your first feature](https://supastack.dev/docs/tanstack/first-feature.md) - A new marketing or landing page → `apps/web-tanstack/src/routes/` — the public website. - A new screen inside the signed-in product → `apps/web-tanstack/src/routes/` for the route plus `packages/web-core/src/pages/` for its load and view sources — pages compose the shared packages. - A new API route → `apps/web-tanstack/src/routes/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/tanstack/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-tanstack/.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 TanStack Start. [Look up a configuration key](https://supastack.dev/docs/tanstack/project-structure.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/tanstack/terminal-basics.md) ``` corepack pnpm --filter @supastack/web-tanstack 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-tanstack/.env.local` feeds the TanStack Start 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-tanstack/.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 TanStack 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/tanstack/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-tanstack/.env.local`; TanStack Start 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-tanstack/src/routes/reports.tsx` and `packages/web-core/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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/agents.md) ## Create the route Add a route file beneath `apps/web-tanstack/src/routes/` — it calls `createFileRoute` to publish the page. Start from the dashboard or content route so you reuse its session and active-workspace resolution; shared page sources (`page.load.ts` and `page.view.tsx`) live under `packages/web-core/src/pages/` and mount through generated bindings. Use the route's loader for protected reads. ``` apps/web-tanstack/src/routes/reports.tsx packages/web-core/src/reports.ts .ai/apps/web-tanstack/src/routes/reports.md .ai/packages/web-core/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/tanstack/security.md) [Entitlement checks](https://supastack.dev/docs/tanstack/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 function that resolves the session, the workspace, and the entitlement in that order — the handler lives in `packages/web-core` and reaches the app through a `createServerFn` binding under `apps/web-tanstack/src/actions/` (regenerate with `corepack pnpm gen:actions`). 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/tanstack/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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/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/tanstack/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 TanStack 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 TanStack web repository. [Walk the agent-driven setup](https://supastack.dev/docs/tanstack/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/packages/web-core/src/reports.md` explains the decisions behind `packages/web-core/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/tanstack/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/tanstack/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/tanstack/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/tanstack/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/tanstack/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/tanstack/apple-sign-in.md) [Add Stripe payments](https://supastack.dev/docs/tanstack/stripe-payments.md) [Add Resend email](https://supastack.dev/docs/tanstack/resend-email.md) [Collect the remaining provider keys](https://supastack.dev/docs/tanstack/provider-keys.md) [Set up database](https://supastack.dev/docs/tanstack/setup-database.md) [Set up payments](https://supastack.dev/docs/tanstack/setup-payments.md) [Set up email](https://supastack.dev/docs/tanstack/setup-email.md) [Set up OAuth sign-in](https://supastack.dev/docs/tanstack/setup-oauth.md) [Set up AI](https://supastack.dev/docs/tanstack/setup-ai.md) [Set up background jobs](https://supastack.dev/docs/tanstack/setup-jobs.md) [Set up file storage](https://supastack.dev/docs/tanstack/setup-storage.md) [Set up observability](https://supastack.dev/docs/tanstack/setup-observability.md) [Spam protection](https://supastack.dev/docs/tanstack/captcha.md) [Set up custom domains](https://supastack.dev/docs/tanstack/setup-domains.md) [Deploy to Google Cloud](https://supastack.dev/docs/tanstack/deploy-google-cloud.md) [Deployment checklist](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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/tanstack/ship-in-5-minutes.md) [Set up the database](https://supastack.dev/docs/tanstack/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-tanstack/.env.local` — TanStack Start 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-tanstack/.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/tanstack/github-sign-in.md) [OAuth provider overview](https://supastack.dev/docs/tanstack/setup-oauth.md) --- Source: [Add GitHub sign-in](https://supastack.dev/docs/tanstack/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/tanstack/ship-in-5-minutes.md) [Set up the database](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack/.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-tanstack/.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/tanstack/setup-database.md) [OAuth provider overview](https://supastack.dev/docs/tanstack/setup-oauth.md) --- Source: [Add Apple sign-in](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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/tanstack/stripe-payments.md) [OAuth provider overview](https://supastack.dev/docs/tanstack/setup-oauth.md) --- Source: [Add Stripe payments](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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 `packages/web-core/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-tanstack/.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-tanstack/.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/tanstack/setup-payments.md) [Add Resend email next](https://supastack.dev/docs/tanstack/resend-email.md) [Mobile purchases use RevenueCat](https://supastack.dev/docs/mobile/revenuecat.md) --- Source: [Add Resend email](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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-tanstack/.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-tanstack/.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/tanstack/setup-email.md) [Collect the remaining provider keys next](https://supastack.dev/docs/tanstack/provider-keys.md) --- Source: [Set up Amazon SES email](https://supastack.dev/docs/tanstack/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-tanstack/.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/tanstack/setup-email.md) --- Source: [Collect the remaining provider keys](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/setup-ai.md) covers that setup. ``` # apps/web-tanstack/.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-tanstack/trigger` and `apps/web-tanstack/trigger.config.ts` reads `TRIGGER_PROJECT_REF`. From `apps/web-tanstack` run `corepack pnpm exec trigger.dev 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-tanstack trigger:deploy` to publish the task set. ``` # apps/web-tanstack/.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-tanstack: cd apps/web-tanstack corepack pnpm exec trigger.dev 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-tanstack/.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 TanStack Start 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-tanstack/.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/tanstack/captcha.md) ``` # apps/web-tanstack/.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/tanstack/deploy-google-cloud.md) [Set up the database next](https://supastack.dev/docs/tanstack/setup-database.md) --- Source: [Deploy to Google Cloud](https://supastack.dev/docs/tanstack/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 `Dockerfile.tanstack` — renamed to `Dockerfile` at the root of the TanStack-only repository — already builds the app with `vite build` into `dist/client` + `dist/server` and runs `node server.mjs` 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/tanstack/setup-database.md) [Create the OAuth accounts first](https://supastack.dev/docs/tanstack/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-tanstack/.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 (or saved as `DATABASE_URL_TRANSACTION`). 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-tanstack/.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. In the full monorepo the file is `Dockerfile.tanstack`, so add `--dockerfile Dockerfile.tanstack` to the submit command there. 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 # TanStack-only checkout: the Dockerfile is named Dockerfile — drop the flag. gcloud builds submit --dockerfile Dockerfile.tanstack --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 image'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/tanstack/deployment.md) --- Source: [Set up database](https://supastack.dev/docs/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.env.local for TanStack Start (PowerShell: Copy-Item .env.example .env; Copy-Item .env.example apps/web-tanstack/.env.local. macOS/Linux/Git Bash: cp .env.example .env; cp .env.example apps/web-tanstack/.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-tanstack/.env.local # macOS/Linux/Git Bash alternative: # cp .env.example .env # cp .env.example apps/web-tanstack/.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-tanstack/.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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack 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 (with optional MARKETPLACE_PLATFORM_FEE_PERCENT for marketplace application fees); 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-tanstack 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 function 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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack 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/tanstack/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-tanstack 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-tanstack/.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/tanstack/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/tanstack/database.md) [Use and troubleshoot email sign-in codes](https://supastack.dev/docs/tanstack/setup-email.md) [Configure Google sign-in](https://supastack.dev/docs/tanstack/setup-oauth.md) [Add Google sign-in](https://supastack.dev/docs/tanstack/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/tanstack/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.env.local for the local Web server and packages/web-core/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-tanstack 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-tanstack 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-tanstack/.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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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/web-core/src/job-handlers.ts and packages/web-core/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-tanstack/.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-tanstack 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-tanstack/trigger.config.ts and the ai.generate task in apps/web-tanstack/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-tanstack 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/tanstack/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-tanstack/.env.local`; TanStack Start 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-tanstack/src/routes/api/storage/avatar.ts`, `apps/web-tanstack/src/routes/api/storage/logo.ts`, `apps/web-tanstack/src/routes/api/storage/organization-logo.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-tanstack/.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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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 `packages/web-core/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/tanstack/project-structure.md) [Upload route inventory](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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/tanstack/security.md) [Diagnose an error in the local terminal](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.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-tanstack 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-tanstack 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/tanstack/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/tanstack/database.md) [Use and troubleshoot email sign-in codes](https://supastack.dev/docs/tanstack/setup-email.md) [Configure Google sign-in](https://supastack.dev/docs/tanstack/setup-oauth.md) [Add Google sign-in](https://supastack.dev/docs/tanstack/google-sign-in.md) [Add GitHub sign-in](https://supastack.dev/docs/tanstack/github-sign-in.md) [Add Apple sign-in](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack 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 functions 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-tanstack 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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack 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/tanstack/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-tanstack/.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/tanstack/project-structure.md) [Set up Stripe in a test environment](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack/.env.local for TanStack Start (PowerShell: Copy-Item .env.example .env; Copy-Item .env.example apps/web-tanstack/.env.local. macOS/Linux/Git Bash: cp .env.example .env; cp .env.example apps/web-tanstack/.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-tanstack/.env.local # macOS/Linux/Git Bash alternative: # cp .env.example .env # cp .env.example apps/web-tanstack/.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-tanstack/.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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack 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/tanstack/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-tanstack/.env.local`; TanStack Start 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-tanstack/src/routes/api/storage/avatar.ts`, `apps/web-tanstack/src/routes/api/storage/logo.ts`, `apps/web-tanstack/src/routes/api/storage/organization-logo.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-tanstack/.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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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 `packages/web-core/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/tanstack/project-structure.md) [Upload route inventory](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/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/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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/web-core/src/job-handlers.ts and packages/web-core/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-tanstack/.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-tanstack 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-tanstack/trigger.config.ts and the ai.generate task in apps/web-tanstack/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-tanstack 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/tanstack/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-tanstack/.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-tanstack/.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/tanstack/setup-email.md) [Set up Amazon SES with AWS SDK or IAM SMTP](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/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 functions 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/tanstack/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-tanstack/.env.local`; TanStack Start 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/setup-ai.md) [Content digests](https://supastack.dev/docs/tanstack/project-structure.md) [Paywall presentation](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/project-structure.md) [Product grid block](https://supastack.dev/docs/tanstack/project-structure.md) [Records table block](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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 `packages/web-core/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-tanstack/public/og.png` and update its alt text and dimensions in `packages/web-core/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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/agents.md) ## Update the site configuration `packages/web-core/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 rasterized OG route (src/framework/og.ts). ## 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/tanstack/theming.md) [Legal page scaffold](https://supastack.dev/docs/tanstack/project-structure.md) [Metadata and social cards](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack dev ``` ## Setup and configuration Route-level metadata comes from the shared page modules — each packages/web-core/src/pages//page.load.ts exports metadata that its file route surfaces through head() via metadataToHead, resolving titles and descriptions from the locale dictionaries so search results stay localized. The src/routes/sitemap[.]xml.ts and src/routes/robots[.]txt.ts routes generate the crawlable surface; the rasterized OG image (src/framework/og.ts) and icon assets carry the social preview. Keep private routes — settings, billing, admin, API — out of the sitemap and disallowed in robots. ``` src/routes/sitemap[.]xml.ts public routes only src/routes/robots[.]txt.ts disallow private surfaces // per page — packages/web-core/src/pages//page.load.ts, // surfaced through the route's head() via metadataToHead 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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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 packages/web-core/src/app-theme.css for signed-in product screens and packages/web-core/src/theme/catalog.ts to add a color pack. Do not edit packages/web-core/src/theme.css for product colors; it controls the marketing brand. 2. From the repository root run corepack pnpm --filter @supastack/web-tanstack 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 packages/web-core/src/app-theme.css for dashboard, auth, settings, billing, chat, and org chrome. Add color packs in packages/web-core/src/theme/catalog.ts. Extend density, radius, font, and layout axes in packages/web-core/src/theme/presets.ts plus matching html[data-*] rules in app-theme.css. Pick the marketing identity in packages/web-core/src/theme/marketing-theme.ts (MARKETING_THEME_MODE: locked | inherit | a pack id) instead of editing packages/web-core/src/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-tanstack dev # In a second terminal, after changing a theme token: corepack pnpm test -- packages/web-core/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 packages/web-core/src/theme/marketing-theme.ts. If text becomes hard to read, restore the previous value and run corepack pnpm test -- packages/web-core/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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack 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-tanstack 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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack 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. The request proxy middleware (src/start.ts) 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-tanstack 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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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 packages/web-core/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-tanstack 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-tanstack dev # Replace with the article slug: # http://localhost:3000/blog/ corepack pnpm test -- packages/web-core/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 surface head 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 packages/web-core/src/resources/content.ts. If the table of contents jumps nowhere, give every section a unique id. Run corepack pnpm test -- packages/web-core/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/tanstack/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-tanstack/.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/tanstack/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/tanstack/terminal-basics.md) [Complete the Web first-run tutorial](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) ``` corepack pnpm --filter @supastack/web-tanstack 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/tanstack/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-tanstack/.env.local`; TanStack Start 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 functions 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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 functions. Preserve workspace access checks when adding rollout logic. [Platform roles](https://supastack.dev/docs/tanstack/admin.md) [Paid access](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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 function 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/first-feature.md) [Server-injected tool context](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack/.env.local`; TanStack Start 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-tanstack/.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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/project-structure.md) [Auth engine differences](https://supastack.dev/docs/tanstack/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. packages/web-core/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/tanstack/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-tanstack/.env.local`; TanStack Start 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/tanstack/before-you-start.md) [Terminal basics](https://supastack.dev/docs/tanstack/terminal-basics.md) [Web local setup](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) [Agent prompts & project memory](https://supastack.dev/docs/tanstack/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/tanstack/feature-flags.md) [Announcements](https://supastack.dev/docs/tanstack/project-structure.md) [Waitlist administration](https://supastack.dev/docs/tanstack/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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack typecheck for Web TypeScript checks. 3. For browser behavior, start the Web app and run corepack pnpm test:e2e:tanstack. 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:tanstack ``` ## 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-tanstack typecheck corepack pnpm test:e2e:tanstack ``` ## 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:tanstack, 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:tanstack if another app owns 3100; this prevents tests from silently targeting another project. --- Source: [Deployment checklist](https://supastack.dev/docs/tanstack/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-tanstack 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/tanstack/deploy-google-cloud.md) [Deploy on a server with Coolify](https://supastack.dev/docs/tanstack/project-structure.md) [Prepare database-backed sign-in](https://supastack.dev/docs/tanstack/database.md) ``` corepack pnpm --filter @supastack/web-tanstack typecheck corepack pnpm --filter @supastack/web-tanstack 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-tanstack typecheck corepack pnpm --filter @supastack/web-tanstack test corepack pnpm --filter @supastack/web-tanstack 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/tanstack/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/tanstack/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/tanstack/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-tanstack 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-tanstack 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-tanstack typecheck corepack pnpm --filter @supastack/web-tanstack 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-tanstack 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-tanstack/.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/tanstack/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 holds the guides for each surface: Web · Next.js and Web · TanStack Start for the browser app on its two engines, Mobile for the phone app and Desktop for the desktop window. You are reading Web · TanStack Start; 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/tanstack/agents.md) [Let an agent run the setup for you](https://supastack.dev/docs/tanstack/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/tanstack/glossary.md) [Update your copy when the kit improves](https://supastack.dev/docs/tanstack/updating-the-kit.md) --- Source: [Words you will see](https://supastack.dev/docs/tanstack/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-tanstack/.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/tanstack/terminal-basics.md) [How to use these docs](https://supastack.dev/docs/tanstack/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/tanstack/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-tanstack, 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-tanstack shown; use supastack-mobile or supastack-desktop as fits). git remote add kit-upstream https://github.com//supastack-tanstack.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/tanstack/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/mobile/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/mobile/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: [Before you start](https://supastack.dev/docs/mobile/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 mobile run. ## 1. Choose what you want to run Use a Windows, macOS or Linux computer with an internet connection, plus a phone or an installed simulator to view the app. The computer performs every installation step below; the phone only displays the finished preview. A repository is the folder of files you received through GitHub. A Mobile purchase provides the Mobile kit repository; the All Platforms bundle provides the monorepo, a folder containing all three apps. Accept the invitation for the repository you purchased. The Mobile repository holds the Expo app and its shared UI packages but not the Web backend — the server that handles accounts and data — so the app connects to a separately running backend. 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. A Mobile checkout shows only the mobile app inside apps; the All Platforms monorepo also shows the web and desktop apps. 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/mobile/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 the Mobile guides 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. Expo Go on your phone is enough for the first preview; native builds and store accounts arrive in the later guides. [Let an agent run the setup for you](https://supastack.dev/docs/mobile/agent-setup.md) [Run the Mobile app](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Meet the Mobile tech stack](https://supastack.dev/docs/mobile/tech-stack.md) --- Source: [Commands, files and configuration](https://supastack.dev/docs/mobile/terminal-basics.md) # Commands, files and configuration Learn where to paste commands, how to open the correct project folder, edit .env files, stop the Expo 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/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 apps/mobile/.env.local for Expo and root .env for database migration commands. Expo does not automatically load root .env. The Web backend reads its own apps/web/.env.local; in a Mobile-only checkout that file belongs to the separate backend repository, not this one. If the backend address changes, update EXPO_PUBLIC_API_URL in apps/mobile/.env.local and restart Expo. 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/mobile/project-structure.md) ## Keep the server running; use another terminal for commands The Expo development server delivers your screens to a phone or simulator while you work. Leave its terminal open when a tutorial says to scan the QR code. A server command keeps running and does not return a fresh prompt until you stop it. 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 Expo settings, stop Expo with Ctrl + C, then run the command below to restart it and clear the cached bundle. Saving a source file normally refreshes the preview automatically, but environment changes need the restart. ``` corepack pnpm --filter @supastack/mobile start --clear ``` ## 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/mobile/troubleshooting.md) --- Source: [Let an agent set it up](https://supastack.dev/docs/mobile/agent-setup.md) # Let an agent set it up Paste one ready-made prompt into a coding agent, let it run the Mobile 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`), per-file memory notes (`.ai/`) and task skills (`.agents/skills/`). Open a terminal inside your copy of the repository to run it — the standalone Mobile checkout does not include the `prompts/` folder, so the ready-made prompt is printed in this guide instead of a file. 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. This path sets up the Expo app and its connection to the Web backend — the server that handles accounts and data. If your checkout also contains apps/web, the agent can run that backend locally; a Mobile-only checkout needs the reachable backend address you were given. [Run the Mobile app by hand instead](https://supastack.dev/docs/mobile/ship-in-5-minutes.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/mobile/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, decide the two things the prompt asks for: the backend address your device can reach — a computer LAN address for a local backend or your provided HTTPS address — and where you will view the app: Expo Go on a phone, an iOS Simulator on a Mac, or an Android emulator. ## Copy the prompt, fill in two blanks, paste The prompt is the code block below — select and copy the whole block. Two placeholders need your choices. Replace `` with the backend address your device can reach — your computer's LAN address plus port 3000 for a local backend on the same Wi-Fi, or your provided HTTPS address. Replace `` with expo-go for a physical phone or simulator for an installed iOS/Android simulator. The numbered steps stay exactly as written. ``` Set up this supastack Mobile checkout end to end and open the Expo app on **** against the backend at ****. 1. Read llms.txt, AGENTS.md and docs/agent-security.md before changing anything. 2. Run corepack pnpm install. 3. Create apps/mobile/.env.local from the names in .env.example: EXPO_PUBLIC_API_URL=, EXPO_PUBLIC_APP_SCHEME=supastack and EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=off. Never invent a variable absent from .env.example. 4. If apps/web exists in this checkout, start it in a second terminal; otherwise verify answers from this computer and report its status. 5. Start Expo with corepack pnpm --filter @supastack/mobile start and report the QR preview or simulator launch with the real output. 6. Stop after any step fails twice and report the failing command and first error. Ask before deleting anything. ``` ## What the agent does with the prompt The prompt points the agent at the map, the rules and the security checklist, then tells it to work through the setup 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 `apps/mobile/.env.local` from `.env.example` names only, with the API address and scheme you supplied — the agent never invents an `EXPO_PUBLIC_` setting absent from the example file. - Verifies the backend answers at your address — starting `apps/web` itself only when that folder exists in this checkout — instead of assuming a server that is not there. - Installs dependencies and starts the Expo development server, then reports the QR preview or simulator launch with the actual output. - Stops and reports after the same step fails twice — it does not work around a failure or delete anything without asking. ## What you check in the agent's report A good report shows evidence, not claims. Look for the backend URL answering, the dependency install completing and the Expo server 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. If your agent keeps improvising after repeated failures, stop it and take the first error to [the troubleshooting guide](https://supastack.dev/docs/mobile/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 preview and real sign-in separate in your head. A successful QR preview shows screens only — real accounts need the backend's database, and real purchases need an EAS development build. 'The preview opened' is not yet 'accounts work'. ## If you would rather do it by hand Every step the agent performs is written out in the manual guides below — the same commands, the same checks, at your pace. [Run the Mobile app in 5 minutes](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Read the full local setup](https://supastack.dev/docs/mobile/local-setup.md) [Fix a setup failure](https://supastack.dev/docs/mobile/troubleshooting.md) --- 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/mobile/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/mobile/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: [Mobile tech stack](https://supastack.dev/docs/mobile/tech-stack.md) # Mobile tech stack The technologies inside the Expo app and the role each one plays. ## Before you begin The Mobile app is a small native surface: every library below is already pinned in `apps/mobile/package.json`, so nothing on this page needs a separate install. This page explains what each piece does so the later guides make sense. The backend keeps its own stack — Next.js, Postgres and the provider adapters — in the Web repository. The app reaches it over `EXPO_PUBLIC_API_URL` instead of embedding any of it, which is why a Mobile-only checkout stays small. ## Expo + React Native Expo is the toolchain that runs, bundles and builds the app; React Native renders real native interface elements rather than a webview. Expo Router gives file-based navigation: every file under `apps/mobile/app/` becomes a screen, with the signed-in stack grouped under `app/(app)/`. Expo Go opens the shared development preview on your phone, while a development build is your own installable app — required once a native module such as purchases or the biometric lock must run. - [Expo](https://expo.dev) - [Expo Router](https://docs.expo.dev/router/introduction/) - [React Native](https://reactnative.dev) ## TypeScript All app code is TypeScript, and the package ships a strict typecheck. You can check every file without a device or a running backend. - [TypeScript](https://www.typescriptlang.org) ## Better Auth Expo + secure storage Sign-in rides the Better Auth Expo client through the system browser: the app opens the backend's auth page, the backend returns to the app's deep-link scheme, and the session token is kept in `expo-secure-store` — the Keychain on iOS and the Keystore on Android — never in plain storage or bundled config. The same modules cover passkeys, an optional biometric app lock through `expo-local-authentication`, and sign-out that clears the workspace-scoped offline cache. - [Better Auth](https://better-auth.com) - [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/) ## RevenueCat purchases `react-native-purchases` connects StoreKit and Play Billing to the kit's entitlement sync: the app starts the store purchase, the backend verifies it through the RevenueCat webhook, and the server decides what the workspace may use. Purchases stay off in the first preview (`EXPO_PUBLIC_MOBILE_PURCHASES_PROVIDER=off`). Real purchase and restore testing needs an EAS development build, because Expo Go cannot run the native purchase module. - [RevenueCat](https://www.revenuecat.com/docs) ## EAS Build + Submit `eas.json` ships three profiles: `development` builds a dev client for the iOS Simulator, `preview` builds an internal Android APK for testers, and `production` builds the store binaries with auto-incremented versions and a Play internal-track submit step. Builds run in Expo's cloud, so you do not need a local Xcode or Android Studio to produce binaries — only to run simulators on your own computer. - [EAS Build](https://docs.expo.dev/eas/) ## Shared packages `@supastack/ui-native` carries the cross-platform view-models — `createEntitlementCard` turns server entitlement state into one card contract — and `@supastack/design-tokens` carries the theme values. Only these two shared packages ship inside the Mobile checkout; the server packages live with the Web backend. ## Verification tooling Deterministic Vitest suites cover session storage, deep links, chat streaming and cancellation, entitlements, purchases, the offline cache and notification routing — they run on your computer without a device. Native-only behavior such as the app lock, push delivery and store purchases is verified in an EAS development build instead. ``` corepack pnpm --filter @supastack/mobile test corepack pnpm --filter @supastack/mobile typecheck ``` --- 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/mobile/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/mobile/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: [Project structure](https://supastack.dev/docs/mobile/project-structure.md) # Project structure Find the routes, the feature modules, and the boundary to the backend. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/mobile/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/mobile/terminal-basics.md). Open the supastack folder at its root — the folder containing package.json. A Mobile checkout contains apps/mobile plus the shared packages below; the All Platforms monorepo adds the Web backend and the Desktop shell alongside. An environment variable is a named setting stored in a local .env file or your hosting dashboard; restart the app after changing it, and keep server secrets out of EXPO_PUBLIC_ values. 1. Open `apps/mobile/app/_layout.tsx` in the editor — the root layout every screen shares. 2. Browse `apps/mobile/src/` — one folder per feature area, each holding its logic and tests. 3. From the repository root run `corepack pnpm --filter @supastack/mobile start` and open the preview on your device. The Expo app is organized as routes that call feature modules. Routes stay thin — they render screens; the logic they call lives under `src/` with its tests beside it. Read AGENTS.md and .ai/PROJECT_CONTEXT.md before changing behavior; the `.ai/` mirror docs record decisions that are not visible from a file alone. ``` apps/mobile/app/ Expo Router routes — sign-in, onboarding, invite, paywall apps/mobile/app/(app)/ Signed-in screens — home, chat, notifications, settings, content, commerce, assistant apps/mobile/src/ Feature modules — auth, api, purchases, offline, push, ui, theme, i18n packages/ui-native/ Shared native view-models packages/design-tokens/ Shared theme tokens apps/mobile/app.config.ts Scheme and allowed-origin derivation apps/mobile/eas.json EAS build and submit profiles docs/ Runbooks incl. store submission .agents/skills/ Task guides for coding agents ``` ## Where a change belongs Before editing, decide which folder owns the change. The rule of thumb: a route file only renders; reusable logic lives in a `src/` module; anything the server must decide never enters the app. [Build your first mobile feature](https://supastack.dev/docs/mobile/first-feature.md) - A new signed-in screen → `apps/mobile/app/(app)/` for the route plus `apps/mobile/src//` for its logic. - A new pre-auth or standalone screen → `apps/mobile/app/` outside the `(app)` group. - A call to the backend → `apps/mobile/src/api/client.ts` — extend the typed client rather than fetching ad hoc. - A visual shared between screens → `apps/mobile/src/ui/` primitives; a cross-platform view-model → `packages/ui-native/`. - Colors, spacing and dark/light values → `apps/mobile/src/theme/` backed by `packages/design-tokens/`. - A new translation key → all four catalogs under `apps/mobile/src/i18n/`. - Anything server-side — data, permissions, provider calls → the Web backend repository, which is a separate checkout for Mobile-only purchases. ## Implementation workflow Start at the route file, follow the feature module it renders, then the API client call — the server decides ownership; the app never re-derives it. The `add-a-mobile-screen` skill in `.agents/skills/` walks this same order. Run the package tests and typecheck after editing a shared package or the API client; the client must keep the same contract the backend serves. ## Verification and troubleshooting If `expo start` reports a missing module, run the install command from the repository root — the app folder alone cannot resolve workspace packages. If a screen appears unstyled, check that it composes `src/ui` primitives instead of raw elements with hardcoded colors. ``` corepack pnpm --filter @supastack/mobile test corepack pnpm --filter @supastack/mobile typecheck ``` --- Source: [Build your first feature](https://supastack.dev/docs/mobile/first-feature.md) # Build your first feature An end-to-end walk from a new screen to server-checked data. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Mobile local setup, and open this repository at its root in your editor. Mobile settings belong in `apps/mobile/.env.local`; Expo does not read the root `.env`. The preview needs a reachable backend for real data. 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 `.agents/skills/add-a-mobile-screen/SKILL.md` — or ask your coding agent to follow it — and create one small screen, for example `apps/mobile/app/(app)/ideas.tsx` with its logic in `apps/mobile/src/ideas/`. 2. Open the new screen on your device through the running preview and confirm it renders for a signed-in member. 3. Check a signed-out and a second-workspace account — anything that must stay private must come back denied or absent, because the server enforces the boundary. For custom code, name the screen, the data it shows, who may see it and the errors to handle. Give your coding agent this task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md` and the `.ai/` mirrors of the files it will touch; list files and checks before editing; and show you the diff. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/mobile/before-you-start.md) [Terminal basics](https://supastack.dev/docs/mobile/terminal-basics.md) [Mobile local setup](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/mobile/agents.md) ## Create the screen Add a route file under `apps/mobile/app/(app)/` — Expo Router turns the file into a screen inside the signed-in stack. Compose it from `apps/mobile/src/ui` primitives (Screen, Card, Title, Detail, AppButton) so it inherits the app's spacing, type and colors. ``` apps/mobile/app/(app)/ideas.tsx apps/mobile/src/ideas/index.ts apps/mobile/src/ideas/index.test.ts ``` ## Read and write through the typed client Fetch through `apps/mobile/src/api/client.ts`, which attaches the stored session to each request against `EXPO_PUBLIC_API_URL` and returns typed errors. Keep payloads identical to the web contract — the same endpoints serve both clients. If the screen reads workspace data, wrap the call with the helpers in `apps/mobile/src/offline/` so a network failure can fall back to the cached copy — only on a network error, never after an auth or permission error. ## Let the server decide ownership The server resolves the signed-in member and the active workspace and scopes every record by `workspaceId`. The app displays what the server returns; it never sends a workspace id the server should trust. If the data you need has no endpoint, add it on the backend first — business rules live on the server. A hidden button in the app does not enforce access. ## Verify access and recovery Check a signed-in member, a member of another workspace, a signed-out session and airplane mode: member data appears, foreign data is denied, a signed-out device returns to sign-in, and the offline copy serves only on a network error. [Offline read cache](https://supastack.dev/docs/mobile/offline.md) [Mobile security checklist](https://supastack.dev/docs/mobile/security.md) ## Polish the states Give the screen loading, empty and error states through the shared primitives, add every new string to all four i18n catalogs, and wire the route into home or settings navigation so a member can actually find it. --- Source: [Build with a coding agent](https://supastack.dev/docs/mobile/agents.md) # Build with a coding agent Hand tasks to a coding agent: the files it reads, the shipped skills, and the review that keeps you in control. ## Beginner walkthrough Prerequisites: Read the two beginner guides below and finish Mobile local setup. 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 `.agents/skills/` in the repository root and copy the skill closest to your task — `add-a-mobile-screen` is a good first try. 2. Paste it into your agent and describe your change, 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, store submission 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/mobile/before-you-start.md) [Terminal basics](https://supastack.dev/docs/mobile/terminal-basics.md) [Mobile local setup](https://supastack.dev/docs/mobile/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, generated to list only what this checkout actually ships | | 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 | | .agents/skills/ | Task runbooks written for agents — the six mobile skills listed below | | docs/agent-security.md | The acceptance checklist — authentication, workspace and permission checks, redacted logs | | docs/mobile-store-submission.md | The store release runbook — accounts, builds, review notes and submission order | | .env.example | The complete environment inventory — the agent never invents a variable name absent from this file | ## The shipped skills Each file under `.agents/skills/` is a complete brief: paste it or let the agent read it, and the kit's rules travel with it — server-first data, no secrets in the bundle, offline as a network-error fallback only. The pasteable `prompts/` library ships only in the Web repository; this checkout carries skills instead. For an unattended setup, use the ready prompt in the agent-setup guide. [Let the agent run the setup](https://supastack.dev/docs/mobile/agent-setup.md) - `add-a-mobile-screen` — add or change a screen, its data flow and its offline behavior - `adding-a-dependency` — add, upgrade or move an npm dependency safely - `reviewing-a-pr` — review a diff for correctness, workspace isolation and secrets - `upgrading-from-upstream` — pull a newer kit release into your app - `verify-changes` — choose and run the right checks before handoff - `writing-unit-tests` — cover new logic with Vitest ## 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/mobile/src/ideas/index.md` explains the decisions behind `apps/mobile/src/ideas/index.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. ## 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/mobile/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: [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/mobile/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/mobile/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: [Passkey sign-in](https://supastack.dev/docs/mobile/passkeys.md) # Passkey sign-in Passwordless sign-in through a browser handoff that returns the session to the app. ## How the handoff works Passkey sign-in starts in the app and finishes in the system browser. The app opens {API URL}/auth?mobile_callback= through WebBrowser.openAuthSessionAsync; after the web page completes any sign-in method it calls GET /api/auth/mobile-handoff, which redirects back to ://auth/callback carrying the session cookie and a state nonce. Each attempt generates a fresh nonce that must round-trip unchanged — a mismatched state parameter is rejected as stateMismatch, so a stale or forged callback cannot inject a session. The returned cookie merges into the same SecureStore jar the Better Auth Expo client uses (supastack.auth_cookie), then the session store is notified and the signed-in app renders. A dismissed browser sheet resolves to a silent cancelled result rather than an error, so keep the sign-in screen unchanged when it happens. ``` # apps/mobile/.env.local EXPO_PUBLIC_APP_SCHEME=supastack # apps/web/.env.local — server side AUTH_PASSKEY_ENABLED=true MOBILE_ALLOWED_ORIGINS= ``` ## Keep the allow-list identical on both sides Both sides validate the callback target: the app accepts only ://auth plus its configured production origins, and the server checks the same list built from EXPO_PUBLIC_APP_SCHEME and MOBILE_ALLOWED_ORIGINS. Callbacks carrying credentials, unknown schemes, or lookalike suffixes are rejected before any cookie moves. In production MOBILE_ALLOWED_ORIGINS is required and must list exact non-local HTTPS origins — wildcards are not accepted. The app ships bundled while the server reads its environment per request, so a scheme or origin change needs both a new app build and a server deploy; changing one side alone makes handoffs fail closed. ## The other passwordless methods The same sign-in screen offers email one-time codes (requestEmailOtp then verifyEmailOtp) and Apple or Google social sign-in through signIn.social with a callbackURL. Passkeys appear when AUTH_PASSKEY_ENABLED=true on the server; passkey and two-factor setup for an account still lives in the web app under Settings → Security. ## Verification and troubleshooting Exercise a cold-start callback, a return to a running app, a cancelled browser sheet, and an expired session on the intended build. The typed failures — passkeyUnavailable, browserFailed, untrustedCallback, missingCookie, malformedCookie, cookieStoreFailed — each describe a distinct break; stateMismatch means the nonce did not round-trip and the attempt was correctly refused. If every handoff is rejected, compare EXPO_PUBLIC_APP_SCHEME in the shipped build with the server's scheme and MOBILE_ALLOWED_ORIGINS — the pair drifts when one side was redeployed without the other. --- 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, open `.agents/skills/` and copy the skill closest to your task — `add-a-mobile-screen` for a new screen. 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/mobile/before-you-start.md) [Terminal basics](https://supastack.dev/docs/mobile/terminal-basics.md) [Mobile local setup](https://supastack.dev/docs/mobile/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/mobile/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, feature requests, push tokens, appearance, commerce, content, credits and usage. 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. [Where server code lives](https://supastack.dev/docs/mobile/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/mobile/push-tokens # register this device for push /api/chat/threads*, /api/chat/stream # chat, same contract as web POST /api/v1/agent-runs # budgeted assistant runs 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: [Deep links and app configuration](https://supastack.dev/docs/mobile/deep-links.md) # Deep links and app configuration The URL scheme, the screen route map, and the bundled configuration contract. ## One env value derives the scheme EXPO_PUBLIC_APP_SCHEME feeds apps/mobile/app.config.ts, which derives the deep-link scheme and the production allowed origins through the same mobile auth configuration the sign-in flow uses. The value is validated — lowercase scheme characters only — and reserved schemes such as http, https, file, javascript, data, exp, and exps are refused. The shipped default is supastack. Both identifiers ship as com.supastack.app in app.json (ios.bundleIdentifier and android.package); rename them before the first store upload alongside the scheme. ``` # apps/mobile/.env.local EXPO_PUBLIC_APP_SCHEME=supastack ``` ## Link shapes the app accepts Deep links are custom-scheme only: ://auth/callback completes a sign-in handoff, and ://invite/ opens the invite screen, which posts the token to the invitations accept route. Universal links are not configured — app.json declares no associatedDomains — so every link resolves through the scheme. The expo-router map under apps/mobile/app: index is the sign-in screen, onboarding and new-organization handle first-run setup, paywall presents purchases, and feedback is a standalone screen that works signed out. Inside the (app) group the tabs are home, chat, notifications, and settings; content, commerce, and assistant are hidden screens reached from home quick actions, and settings holds its own sub-stack of screens. ## Bundled means both sides must agree Public Expo values are baked into the distributed binary, while the server validates mobile callbacks against MOBILE_ALLOWED_ORIGINS per request. The pair must match: a scheme or origin change means a new app build and a server deploy, never one or the other. ## No over-the-air channel The kit ships no expo-updates dependency and no updates configuration — JavaScript changes reach users only through new store builds. Keep the API compatible with the oldest supported build, or gate new capability behind server-driven state. ## Verification and troubleshooting Test cold-start links, links arriving while the app runs in the background, and a link received while signed out. If a link opens a store listing or a browser instead of the app, the installed build's scheme does not match EXPO_PUBLIC_APP_SCHEME. --- 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/mobile/before-you-start.md) [Learn commands, files and configuration](https://supastack.dev/docs/mobile/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: [Workspaces and members](https://supastack.dev/docs/mobile/workspaces.md) # Workspaces and members Switch workspaces, manage the organization, and handle invitations. ## Resolve the active workspace GET /api/workspaces returns the member's workspace memberships and the active workspace id; POST with a workspace id switches. The server resolves context from the selection cookie to the stored preference to the personal workspace and verifies live membership, so a forged or stale selection cannot read another workspace. ## Manage the organization GET /api/mobile/organization returns members, pending invitations, seat usage, and the caller's capabilities — canManage and canLeave; a personal workspace answers notOrganization instead. POST creates an organization, PATCH renames it (owner only), and DELETE removes it (owner only, confirmed by typing the name). Organization screens live under Settings → Members and Settings → General; the member's own display name edits go through the Better Auth profile route and accept one to one hundred characters. ## Invite and accept POST /api/mobile/invitations emails an invitation with role admin, member, or guest — owner and admin only. Typed failures cover alreadyMember, alreadyInvited, seatLimitExceeded, dailySendLimit, and invalidInput, so the interface can explain each refusal. Resend and revoke live on the invitation id routes, and resend is rate-limited. Acceptance arrives through the ://invite/ deep link: the app posts the token to the invitations accept route, which returns the workspace id, activates that workspace, and records the preference server-side. Errors cover expired, emailMismatch, not-pending, and seat-limit-exceeded. ## Change roles and remove members PATCH /api/mobile/members/:userId assigns owner, admin, member, or guest; DELETE removes a member; POST /api/mobile/members/leave leaves voluntarily. The sole owner cannot leave or be demoted, members cannot remove themselves through the admin route, and a role change that alters nothing is rejected rather than recorded. ## Verification and troubleshooting Test seat limits, a revoked invitation link, a guest role's reduced actions, and switching back to the personal workspace. A removed member's cached selection must fail closed on the next request, not keep reading the old workspace. --- Source: [AI chat and the assistant](https://supastack.dev/docs/mobile/chat.md) # AI chat and the assistant Streaming chat threads and one-shot agent runs over the shared API. ## Chat threads use the web contract The chat tab calls the same REST surface as the browser app: list and create on /api/chat/threads, and read, rename, or delete on /api/chat/threads/:id. Titles normalize to the shared 120-character limit, and the thread list is covered by the offline read cache. ## Streamed replies Replies stream through POST /api/chat/stream over expo/fetch; server-sent frames become chunk, done, or error events and malformed frames are skipped. Model aliases are fast, smart, and cheap — fast is the default — a structuredOutput flag forwards the matching requirement, and the stop control aborts the in-flight request. ## One-shot agent runs The hidden assistant screen posts {alias, prompt, workspaceId, maxSteps: 4} to /api/v1/agent-runs — a single budgeted run in the active workspace. The client validates the response strictly and rejects a run attributed to a different workspace. Rate limits, sign-in, missing entitlement, and forbidden answers each map to a distinct state, and the run history keeps the last ten in session memory with microunit-exact cost formatting. ## Verification and troubleshooting Test a cancelled stream, a revoked session mid-run, an entitled and a locked workspace, and airplane-mode behavior on the cached thread list. A run denied by the server must surface as unavailable work, never as a silent empty reply. --- Source: [Notifications and push](https://supastack.dev/docs/mobile/notifications.md) # Notifications and push The in-app inbox, channel preferences, and the Expo push pipeline. ## The inbox and channel preferences The notifications tab lists workspace notifications from GET /api/notifications and marks them read with a PATCH on the notification id. Settings → Notifications edits the shared channel preferences — emailBilling, emailInvites, inAppProduct — through GET and PATCH on the same route's preferences view, the exact contract the web app uses. The unread count surfaces on the home screen. ## Register a device for push On sign-in — and again on every workspace switch — the app requests notification permission, resolves the EAS project id from the app config, obtains an Expo push token, and posts it to POST /api/mobile/push-tokens. Android registers on the default channel at high importance. Registration reports registered, permission_denied, or unavailable and never throws — sign-in is never blocked by push. Expo Go and simulators cannot complete registration, so real coverage needs a development build. The token is kept in SecureStore (supastack.push.token) so sign-out can unregister it before the session cookie is destroyed. ## Server delivery and receipts GET lists the member's tokens, POST upserts a token under the active workspace, and DELETE revokes — list and revoke stay user-scoped so a workspace-less account can still clean up. Each user keeps at most twenty device tokens; registering one more prunes the oldest. Every in-app workspace notification is also pushed best-effort: the server sends to the Expo push API in chunks of one hundred, records ok tickets, and prunes DeviceNotRegistered tokens immediately. The optional server-only EXPO_ACCESS_TOKEN adds a Bearer header to those calls and is never logged. A platform schedule reconciles receipts every thirty minutes and sweeps stale tickets — it needs the same scheduler tick driver as the other platform schedules. ``` # Optional server-only token for authenticated Expo push sends: # apps/web/.env.local EXPO_ACCESS_TOKEN= ``` ## Notification taps open app routes Senders attach a web path as data.href. The app validates it — absolute slash paths only, bounded length, no traversal — and maps known paths to native routes, for example /billing to /settings/billing and /dashboard to /home. A tap that arrives while signed out is buffered and flushed after sign-in completes. ## Verification and troubleshooting Test on a development build: grant and deny permission, toggle push under Settings → Notifications — the screen reports this device as enabled, disabled, or denied — and tap a notification cold and warm. If switching off shows an error, the server-side unregister failed and the device stays registered until it succeeds. [Create a development build](https://supastack.dev/docs/mobile/eas-setup.md) --- Source: [Usage and credits](https://supastack.dev/docs/mobile/usage.md) # Usage and credits Metered limits, prepaid credit, and the billing surface. ## Read the usage state GET /api/mobile/usage returns the AI budget as microunit decimal strings — budgetMicrounits, committedMicrounits, heldMicrounits, and keyMode — the prepaid credit balance, per-meter rows with label, used, limit, cap, planLimit and description, and the current period bounds. Presentation marks a meter warning at eighty percent and danger at one hundred. Values are decimal strings, not JSON numbers — parse them before comparing or formatting so large budgets stay exact. ## Prepaid credit ledger GET /api/mobile/credits returns the workspace's prepaid balance and its last fifty ledger rows. The balance feeds Settings → Billing and the home usage card; ledger reference ids are withheld from the client payload. ## Home and billing surfaces The home screen reuses GET /api/workspace/metrics — the same stat grid the web dashboard renders: monthly AI spend, task and thread counts, members, pending invitations, plan status, and unread notifications. Its usage card shows the AI row plus finite-limit meters, and quick-action tiles link to chat, assistant, content, commerce, notifications, feedback, and settings. Settings → Billing shows the plan, expiration, credit balance, and usage meters, explains store-versus-web billing, and links to the paywall. A server schedule sweeps workspaces every fifteen minutes and notifies owners and admins once a month at eighty and one hundred percent of the AI budget. ## Verification and troubleshooting Check a meter near its cap, an exhausted credit balance, and a workspace on the default budget. A blocked action must point at the meter that denied it, not at a generic error. [Native purchase and restore](https://supastack.dev/docs/mobile/revenuecat.md) --- Source: [Content management](https://supastack.dev/docs/mobile/content.md) # Content management Create, edit, and publish workspace content from the app. ## The content surface GET /api/mobile/content lists the workspace's entries with a per-row writable flag — entry bodies stay on the server in the list view. POST creates a draft, GET /api/mobile/content/:id returns the body for editing, PATCH updates fields and status together — draft, published, or archived — and DELETE removes the entry. ## Rules the server enforces Writing requires a workspace member or better, so guests are denied. Bodies cap at one MiB, a blank slug derives from the title, and entries carry locale, excerpt, and requiredEntitlement fields for gated publishing. The list view is covered by the offline read cache. ## Verification and troubleshooting Create a draft, publish it, archive it, and confirm a guest cannot write. The mobile form is for quick edits — long-form authoring and scheduling stay in the web content surface. --- Source: [Commerce on mobile](https://supastack.dev/docs/mobile/commerce.md) # Commerce on mobile Manage store products and orders for the workspace. ## Products GET and POST on /api/mobile/commerce/products list and create products — owner and admin only. A new product starts as a draft; prices enter as strings and store as cents with two-decimal precision, and inventory tracking is optional. Products are never deleted: archived is the removal state, and PATCH updates fields and status — draft, active, archived — together. ## Orders move forward only GET /api/mobile/commerce/orders lists orders; they are created by the store checkout, never by the app. PATCH advances the state forward-only — pending to paid to fulfilled — with cancelled allowed before payment and refunded after. Cancelling restocks tracked inventory in the same transaction, and an illegal transition is a silent no-op rather than an error the user must dismiss. ## Verification and troubleshooting Sell through the public checkout, advance the order on mobile, and confirm stock returns when a pending order is cancelled. Members below admin cannot reach the surface at all — verify they see it as unavailable rather than empty. --- Source: [Developer tools](https://supastack.dev/docs/mobile/developers.md) # Developer tools API keys, webhooks, schedules, domains, and the audit log from the app. ## API keys GET and POST on /api/mobile/api-keys list and create workspace API keys; DELETE revokes by id. Creation returns the plaintext key once and it is never shown again. Keys take a read or write scope and an expiresInDays clamp of zero to 3650 — zero never expires. Owner and admin only. ## Webhooks and deliveries GET and POST on /api/mobile/webhooks manage endpoint subscriptions; creation returns the signing secret once. Each endpoint supports deletion, a deliveries list with attempt counts, response codes, and the last error, and a ping action. Thirteen event types cover AI tasks, billing, commerce orders, connected accounts, content, domains, feature requests, invites, members, and Stripe Connect. ## Workspace schedules GET and POST on /api/mobile/schedules list and upsert schedules keyed on workspace and type — {cron, enabled}; DELETE removes by id. Cron expressions run in UTC, toggling a schedule must resend its cron because upsert overwrites, and missed runs accumulate on the schedule record. ## Custom domains GET and POST on /api/mobile/domains list configured domains with their CNAME target and an enabled flag that reports off when the provider is not configured. DELETE removes a domain and POST refresh re-checks its DNS. ## Audit log GET /api/mobile/audit returns the workspace's audit rows — owner and admin — each with a label, the actor's email, and the target type. The client asks for a hundred rows by default. ## Verification and troubleshooting Create a key, fire a webhook ping, and toggle a schedule. All five surfaces are owner/admin territory — verify a plain member is denied rather than shown stale data. --- Source: [Feature board](https://supastack.dev/docs/mobile/feature-board.md) # Feature board Collect and vote on feature requests — even signed out. ## The board surface The feedback screen stands alone outside the tab group and builds its own auth client, so it opens for signed-out visitors behind a sign-in prompt instead of an error. GET /api/mobile/feature-requests returns the board — the caller's canWrite flag, the request list, and the ids the member already voted on. ## Posting and voting rules POST creates a request with a title and optional details, and POST on the vote route toggles the member's vote. Workspace members and above can submit and vote; guests are refused. Statuses run open through under_review, planned, in_progress, shipped, and closed; the board sorts by votes then recency, and a new request records the author's vote, an audit event, and a feature_request.created webhook in one transaction. ## Verification and troubleshooting Vote and unvote, submit a request, and check a guest's denied write. A signed-out visitor must see the board with a sign-in prompt — a hard error there means the standalone auth client regressed. --- Source: [Offline access](https://supastack.dev/docs/mobile/offline.md) # Offline access A workspace-scoped read cache that keeps screens useful without a network. ## What the cache stores Under the app's document directory, JSON files in supastack-offline/ hold recent successful reads, named by a hash of the user, workspace, and key — raw identifiers never appear in paths. Entries expire after seven days, cap at 256 KB each, and corrupted or oversized files are deleted or refused. All cache I/O is best-effort and never blocks a screen. ## Network first, cache as fallback Cached reads always try the network and persist a fresh copy. The cache serves only on connectivity failures — an unreachable host or a status-0 client error. HTTP failures like 401 or 500 rethrow: denied resources never serve stale data, and a dead session is never extended by a cached reply. ## When the cache is cleared Sign-out clears everything, and signing in as a different account does too. Switching workspaces keeps the cache — scopes are isolated, so one workspace never reads another's rows. ## What the user sees Cached screens show a localized banner — You're offline, showing data saved some time ago — announced as an accessibility alert with relative times. The home, billing, notifications, content, commerce, and chat list screens read through the cache. ## Verification and troubleshooting Airplane-mode test each cached screen: reads survive, writes fail cleanly, and the banner reports the saved age. Sign out and confirm nothing of the previous account remains. --- Source: [App lock and device security](https://supastack.dev/docs/mobile/security.md) # App lock and device security Biometric unlock, session management, and connected accounts. ## Enable the app lock Settings → Security offers a biometric app lock through expo-local-authentication — Face ID, Touch ID, or fingerprint, with the device PIN as an allowed fallback. Enabling requires one successful authentication; disabling is free. The Face ID usage string ships in app.json. ## When the lock engages The gate locks on cold start and after more than sixty seconds in the background. Signed-out users are never locked, and the brief iOS active state that follows a biometric prompt cannot re-lock the app accidentally. ## The lock screen It auto-prompts once per mount, offers a manual Unlock retry, and keeps a Sign out escape. Signing out unregisters the push token before destroying the session — the unregister call needs the live cookie — and a failed sign-out keeps the gate closed with an error instead of opening the app. ## Sessions, connected accounts, and export The same screen lists sessions through the auth client — newest first with the current one flagged — and supports revoking one session or every other session. Connected accounts list under Settings → Connections; the last sign-in method cannot be disconnected, and commerce or Stripe Connect management stays on the web. GET /api/account/export downloads the member's full data export. ## Verification and troubleshooting Lock and unlock, background the app past the grace period, revoke a second session, and sign out from the lock screen. A device without enrolled biometrics cannot enable the lock — the toggle must explain that rather than silently fail. --- 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/mobile/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/mobile/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/mobile/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/mobile/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: [Localize the mobile app](https://supastack.dev/docs/mobile/i18n.md) # Localize the mobile app Four shipped locales, a type-checked catalog, and English fallback. ## How the catalogs work The app ships English, German, Spanish, and French catalogs in apps/mobile/src/i18n — one type-checked MobileCatalog per locale, looked up with t(key, vars) through dotted paths and {name} interpolation. A key missing in one locale falls back to its English entry, and pluralized strings select their form through Intl.PluralRules. ## Picking the locale The device locale resolves through Intl APIs at runtime, matches by language prefix, and falls back to English — a pt-BR device sees English because Portuguese is not in MOBILE_LOCALES. ## Adding strings and locales Add every new key to all four catalogs — the shared catalog type fails typecheck when a locale misses one. A fifth locale means extending MobileLocale and MOBILE_LOCALES and writing a complete catalog. Mobile catalogs are separate from the web app's i18n: nothing syncs automatically, so copy that exists on both platforms is translated in both places. ## Verification and troubleshooting Switch the device language through each shipped locale plus an unsupported one, and check a pluralized string like the offline banner's relative times in every catalog. A raw dotted key on screen means the key is missing everywhere, not untranslated in one locale. --- Source: [Configure the mobile app](https://supastack.dev/docs/mobile/configuration.md) # Configure the mobile app Set the backend address and deep-link scheme the native build uses, and keep both sides of the handoff in sync. ## Setup and configuration Put device-facing values in `apps/mobile/.env.local`; Expo reads that file, not the repository-root `.env`. `EXPO_PUBLIC_API_URL` is the Web backend origin the phone can reach — the same value you tested in the phone browser. `EXPO_PUBLIC_APP_SCHEME` is the app's native URI scheme; use a reverse-DNS value such as `com.example.app`. `app.json` owns the product name, slug, version, bundle identifier, and plugins. `app.config.ts` derives the deep-link scheme and production origins from `EXPO_PUBLIC_APP_SCHEME`, so a scheme change needs a native rebuild, not only a Metro restart. On the server, add every origin the app will use to `MOBILE_ALLOWED_ORIGINS`; the handoff fails closed when the two sides disagree. Every `EXPO_PUBLIC_*` value is bundled into the app. Keep provider keys on the server and use public values only for addresses and schemes. ``` EXPO_PUBLIC_API_URL=http://192.168.1.25:3000 EXPO_PUBLIC_APP_SCHEME=com.example.app ``` ## Implementation workflow Restart Expo after changing `.env.local`. Run the package tests and typecheck, then confirm the resolved scheme with a development build: open an auth callback link and check that it returns to the app instead of the browser. Expo Go cannot validate custom schemes belonging to your native build. ## Verification and troubleshooting Restart Expo after changing `.env.local` and open the preview on the target device. Change the theme, restart the app, and confirm the choice persists; then trigger a deep link and check that the app routes it instead of the browser. --- Source: [Test the mobile app](https://supastack.dev/docs/mobile/testing.md) # Test the mobile app Run the deterministic suites, then verify native sign-in, links, security, and purchases on a real development build. ## Setup and configuration The mobile package ships deterministic Vitest suites for session storage, deep links, chat streaming and cancellation, entitlements, purchases, offline cache, and notification routing. They run on your computer without Expo, a phone, provider credentials, or device setup. ``` corepack pnpm --filter @supastack/mobile test corepack pnpm --filter @supastack/mobile typecheck ``` ## Implementation workflow Verify native behavior in an EAS development build on a real target: sign in and out, restore a session after restarting the app, open a deep link, enable the app lock, turn the network off and on, and register push. Store purchases and the app lock cannot be proven in Expo Go. ## Verification and troubleshooting Run the production build profile against a staging backend before submitting. Check the bundle identifier, the version source, the declared permissions, and that no provider secret appears in the app bundle. Record the exact device and OS version used for each release check. --- Source: [Troubleshooting the mobile app](https://supastack.dev/docs/mobile/troubleshooting.md) # Troubleshooting the mobile app Fix the common preview, configuration, and native-build failures before they reach a tester. ## Setup and configuration If the app cannot reach the backend, remember that `localhost` on a phone means the phone. Use the computer's LAN address, `10.0.2.2` for the Android emulator, or an HTTPS staging URL. Open that address in the phone browser first; check Wi-Fi, firewall, and VPN before changing code. An Expo tunnel carries the screen bundle, not your API. A missing or wrong `EXPO_PUBLIC_API_URL` surfaces as a configuration error before sign-in. A rejected callback usually means the scheme in the build and the server's `MOBILE_ALLOWED_ORIGINS` disagree, or the callback ran in Expo Go instead of the native build. ## Implementation workflow After a native dependency or scheme change, stop Metro, clear its cache, rebuild the development client, and reinstall the app. If only the JavaScript changed, reloading the preview is enough. Keep the Expo SDK version in the app aligned with the Expo Go or development build installed on the device. ## Verification and troubleshooting The preview, sign-in, and deep link work on the target device. --- 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/desktop/before-you-start.md) [Learn how to run a command and edit a file](https://supastack.dev/docs/desktop/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: [Before you start](https://supastack.dev/docs/desktop/before-you-start.md) # Before you start Install Node.js, Git, an editor, Rust and the native build tools, accept your repository invitation, download the kit and check your computer is ready for the first desktop run. ## 1. Choose what you want to run Use a Windows, macOS or Linux computer with an internet connection. The Desktop app is a native window installed on this same computer — no phone is involved. The steps below install its tools and download the project. A repository is the folder of files you received through GitHub. A Desktop purchase provides the Desktop kit repository; the All Platforms bundle provides the monorepo, a folder containing all three apps. Accept the invitation for the repository you purchased. The Desktop repository holds the Tauri shell — the native window around your web app — but not the Web backend, so the window connects to a separately running backend address. 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. A Desktop checkout shows only the desktop shell inside apps; the All Platforms monorepo also shows the web and mobile apps. 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/desktop/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. Install Rust and the native build tools 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. [Tauri's operating-system prerequisite checklist](https://tauri.app/start/prerequisites/) [Official Rust installer](https://rustup.rs/) ``` rustc --version cargo --version ``` ## 8. Continue with the Desktop guides You are ready when Node and Git print versions, Corepack can select the pinned pnpm, rustc and cargo print versions, and your terminal is in the folder containing package.json. Pick one tutorial below. [Let an agent run the setup for you](https://supastack.dev/docs/desktop/agent-setup.md) [Run the Desktop app](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Meet the Desktop tech stack](https://supastack.dev/docs/desktop/tech-stack.md) --- Source: [Commands, files and configuration](https://supastack.dev/docs/desktop/terminal-basics.md) # Commands, files and configuration Learn where to paste commands, how to open the correct project folder, edit .env files, stop the desktop dev command 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/desktop, 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 and apps/web/.env.local for the Web backend — in a Desktop-only checkout that file belongs to the separate backend repository. The desktop shell does not read .env files: SUPASTACK_APP_URL is a terminal environment setting you pass on the command line when starting it, not configuration stored in a file. 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/desktop/project-structure.md) ## Keep the dev command running; use another terminal for commands The desktop development command compiles the Rust shell and opens the window while it runs. Leave its terminal open while you test the window. A server command keeps running and does not return a fresh prompt until you stop it. 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 changing shell settings, stop the desktop command with Ctrl + C and run it again. The Rust core recompiles on each start; changing SUPASTACK_APP_URL means passing the new value on the next run, not editing a saved file. ``` corepack pnpm --filter @supastack/desktop 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/desktop/troubleshooting.md) --- Source: [Let an agent set it up](https://supastack.dev/docs/desktop/agent-setup.md) # Let an agent set it up Paste one ready-made prompt into a coding agent, let it run the Desktop 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`), per-file memory notes (`.ai/`) and task skills (`.agents/skills/`). Open a terminal inside your copy of the repository to run it — the standalone Desktop checkout does not include the `prompts/` folder, so the ready-made prompt is printed in this guide instead of a file. 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. This path sets up the Tauri shell and its connection to the Web backend — the server that handles accounts and data. The desktop window only displays that backend; if your checkout also contains apps/web, the agent can run it locally, and a Desktop-only checkout needs the reachable backend address you were given. [Run the Desktop app by hand instead](https://supastack.dev/docs/desktop/ship-in-5-minutes.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/desktop/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, decide two things the prompt asks for: the backend address the window should load — http://localhost:3000 for a local backend or your provided HTTPS address — and whether Rust and the native build tools are already installed on this computer. ## Copy the prompt, fill in two blanks, paste The prompt is the code block below — select and copy the whole block. Two placeholders need your choices. Replace `` with the backend address the window should load — http://localhost:3000 for a local backend or your provided HTTPS address. Replace `` with present when rustc and cargo already print versions, or install when the agent should set up Rust and the OS build tools first. The numbered steps stay exactly as written. ``` Set up this supastack Desktop checkout end to end and open the Tauri window against the backend at ****. 1. Read llms.txt, AGENTS.md and docs/agent-security.md before changing anything. 2. Run rustc --version and cargo --version. On **** = install, install Rust and this OS's Tauri prerequisites first, then continue. 3. Run corepack pnpm install. 4. If apps/web exists in this checkout, start it in a second terminal; otherwise verify answers from this computer and report its status. 5. Start the shell with corepack pnpm --filter @supastack/desktop dev and report whether the window opens, the tray icon appears and the summon shortcut works. 6. Stop after any step fails twice and report the failing command and first error. Ask before deleting anything. ``` ## What the agent does with the prompt The prompt points the agent at the map, the rules and the security checklist, then tells it to work through the setup 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. - Checks the Rust toolchain and this OS's Tauri prerequisites, and reports what it installs rather than failing halfway through a compile. - Verifies the backend answers at your address — starting `apps/web` itself only when that folder exists in this checkout — instead of assuming a server that is not there. - Installs dependencies, compiles the shell and opens the window, then reports window, tray and summon-shortcut behavior with the actual output. - Stops and reports after the same step fails twice — it does not work around a failure or delete anything without asking. ## What you check in the agent's report A good report shows evidence, not claims. Look for the backend URL answering, the dependency install completing, a finished Rust compile and an open window 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. If your agent keeps improvising after repeated failures, stop it and take the first error to [the troubleshooting guide](https://supastack.dev/docs/desktop/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 shell and real sign-in separate in your head. The window showing your backend's homepage proves the shell — real accounts still need the backend's database. 'The window opened' is not yet 'accounts work'. ## If you would rather do it by hand Every step the agent performs is written out in the manual guides below — the same commands, the same checks, at your pace. [Run the Desktop app in 5 minutes](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Read the full shell setup](https://supastack.dev/docs/desktop/run.md) [Fix a setup failure](https://supastack.dev/docs/desktop/troubleshooting.md) --- 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, open `.agents/skills/` and copy the skill closest to your task — `add-a-desktop-capability` for shell behavior. 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/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/desktop/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 plus opener:allow-open-url for system-browser links) keep the webview sandboxed; widen permissions in src-tauri/capabilities only when a native feature needs them. --- Source: [Desktop tech stack](https://supastack.dev/docs/desktop/tech-stack.md) # Desktop tech stack The technologies inside the Tauri shell and the role each one plays. ## Before you begin The Desktop app is deliberately thin: a Rust core owns the native window while a webview loads your web app. Its setup is already pinned in the checkout — this page explains what each piece does so the later guides make sense. Product UI, authentication, billing and AI all live in the web codebase the window loads. A Desktop-only checkout contains only the shell — which is why this stack is small on purpose. ## Tauri 2 + the system webview Tauri 2 renders the app with the operating system's own webview — WebView2 on Windows, WebKit on macOS, WebKitGTK on Linux — so the shell stays small and carries no bundled browser runtime. `frontendDist` points at `apps/desktop/ui`, the boot page shown while the web app loads, so a cold start never renders a blank window. - [Tauri](https://tauri.app) ## Rust + TypeScript The shell's behavior is Rust — `src-tauri/src`, edition 2021, minimum Rust 1.90 — built by cargo. Small Node scripts written in TypeScript handle the release preflight and the updater manifest. - [Rust](https://www.rust-lang.org) ## Native features via plugins Window state, single instance, deep links, the global shortcut, the tray, launch at login, the updater, external-link opening, dialogs and process control all come from `tauri-plugin-*` crates — the shell adds no custom IPC commands of its own. Adding a native capability means registering another plugin or wiring the ones already present. ## tauri.conf.json + capabilities The bundle identifier `com.supastack.desktop`, the window list, plugin options and the updater endpoint live in `src-tauri/tauri.conf.json`. Webview permissions stay in `src-tauri/capabilities/default.json` — currently `core:default` plus `opener:allow-open-url` for system-browser links — and are widened only when a native feature needs them. ## Verification tooling cargo test covers the Rust logic — URL resolution, the navigation gate, deep-link mapping, updater channels, zoom and window preferences — while `node --check` validates the boot page and `check:release-config` gates release builds. CI builds all three operating systems. ``` corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop check:release-config ``` --- 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, open `.agents/skills/` and copy the skill closest to your task — `add-a-desktop-capability` for shell behavior. 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/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/desktop/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. A path-only link like `supastack:///settings/appearance?preview=1#main` maps directly to `/settings/appearance?preview=1#main`, while an authority link like `supastack://workspace/settings` maps to `/workspace/settings`; `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/settings/appearance?preview=1#main ``` ## 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: [Project structure](https://supastack.dev/docs/desktop/project-structure.md) # Project structure Find the Rust shell, the boot page, the release scripts and the boundary to the web app. ## Setup and configuration Before any step, read [Before you start](https://supastack.dev/docs/desktop/before-you-start.md) and [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md). Open the supastack folder at its root — the folder containing package.json. A Desktop checkout contains apps/desktop and its release scripts; the All Platforms monorepo adds the Web backend and the mobile app alongside. An environment variable is a named setting stored in a local .env file or your hosting dashboard; the shell reads SUPASTACK_APP_URL as a terminal setting, not from a file. 1. Open `apps/desktop/src-tauri/src/lib.rs` — every native behavior lives in this one file. 2. Browse `apps/desktop/src-tauri/` — configuration, capabilities and icons. 3. From the repository root run `corepack pnpm --filter @supastack/desktop dev` with a reachable backend. The checkout contains the Tauri shell only: Rust source, its config, the boot page and the release scripts. Product screens live in the web app the window loads — a Desktop-only checkout has no app screens of its own. Read AGENTS.md and .ai/PROJECT_CONTEXT.md before changing behavior; the `.ai/` mirror docs record decisions that are not visible from a file alone. ``` apps/desktop/src-tauri/src/ Rust — window, tray, menus, deep links, updater apps/desktop/src-tauri/ tauri.conf.json, capabilities/, Cargo.toml, icons apps/desktop/ui/ Boot page (index.html + boot.js) shown while loading apps/desktop/scripts/ Release preflight + update manifest docs/ Runbooks incl. desktop updates .agents/skills/ Task guides for coding agents ``` ## Where a change belongs Before editing, decide which side owns the change: anything a user sees as product UI belongs to the web app; only OS-level behavior belongs in Rust. [Build your first desktop feature](https://supastack.dev/docs/desktop/first-feature.md) - Window, tray, menu, shortcut or single-instance behavior → `apps/desktop/src-tauri/src/lib.rs`. - A new webview permission → `apps/desktop/src-tauri/capabilities/default.json` — widen only when a native feature needs it. - Bundle identifier, version, updater endpoint or plugin options → `apps/desktop/src-tauri/tauri.conf.json`. - Placeholder product icons → replace `placeholder-icon.png` and run the icons script. - Boot, loading or offline-overlay behavior → `apps/desktop/ui/`. - Any product screen, setting or data → the web app repository — the shell never forks product UI. ## Implementation workflow For a native change, edit `lib.rs`, run `cargo check` and the Rust tests, then exercise the window on each target OS — tray, shortcut and deep-link behavior differs per platform. The `add-a-desktop-capability` skill in `.agents/skills/` walks this same order. ## Verification and troubleshooting `cargo check` catches compile errors and `cargo test` covers the pure logic. Real verification is the running window on Windows, macOS and Linux — tray, shortcut and deep links cannot be proven by tests alone. ``` corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop typecheck ``` --- Source: [Build your first feature](https://supastack.dev/docs/desktop/first-feature.md) # Build your first feature An end-to-end walk from one native shell change to a verified window. ## Beginner walkthrough Prerequisites: Read the two beginner guides below, finish Desktop local setup, and open this repository at its root in your editor. The shell reads SUPASTACK_APP_URL as a terminal setting when it starts — not from a saved .env file. 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 `.agents/skills/add-a-desktop-capability/SKILL.md` — or ask your coding agent to follow it — and make one small native change, for example a new View-menu entry or a different summon shortcut. 2. Run `cargo check`, start the shell and exercise the change in the open window. 3. Repeat on each operating system you plan to ship — tray, shortcut and menu behavior differs per platform. For custom code, name the behavior, the menu or shortcut it affects and what should happen on each OS. Give your coding agent this task and ask it to read `AGENTS.md`, `.ai/PROJECT_CONTEXT.md` and the `.ai/` mirrors of the files it will touch; list files and checks before editing; and show you the diff. Use sample values; never paste a real secret. [Before you start](https://supastack.dev/docs/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/desktop/agents.md) ## Decide native or product first Anything a user experiences as product UI — screens, data, settings — belongs to the web app and arrives in the window automatically. Only OS-level behavior belongs in Rust: windows, tray, menus, global shortcuts, launch at login and update checks. ``` apps/desktop/src-tauri/src/lib.rs apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/capabilities/default.json ``` ## Keep the webview sandbox tight The webview can only call what `capabilities/default.json` lists. A change that needs a new plugin adds the crate in `Cargo.toml`, registers it in `lib.rs` and grants the minimum permission in capabilities — never a wildcard. Persisted shell state — window position, zoom, update channel, window preferences — lives in the app's data directory through the shipped sidecars, not in files you manage by hand. ## Verify on every target OS Run cargo test, then a real window on Windows, macOS and Linux: window-state restore, close-to-tray, single-instance focus and the summon shortcut with your change active. One OS passing does not prove the other two. [Native menus and window preferences](https://supastack.dev/docs/desktop/menus.md) [Desktop testing checklist](https://supastack.dev/docs/desktop/testing.md) --- Source: [Build with a coding agent](https://supastack.dev/docs/desktop/agents.md) # Build with a coding agent Hand tasks to a coding agent: the files it reads, the shipped skills, and the review that keeps you in control. ## Beginner walkthrough Prerequisites: Read the two beginner guides below and finish Desktop local setup. 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 `.agents/skills/` in the repository root and copy the skill closest to your task — `add-a-desktop-capability` is a good first try. 2. Paste it into your agent and describe your change, 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, releasing installers 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/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/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, generated to list only what this checkout actually ships | | AGENTS.md | The non-negotiable boundaries: local work only until you approve more, the shell never forks product UI | | .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 | | .agents/skills/ | Task runbooks written for agents — the five desktop skills listed below | | docs/agent-security.md | The acceptance checklist — authentication, workspace and permission checks, redacted logs | | docs/desktop-updates.md | The update-channel runbook — signing keys, endpoints and release channels | | .env.example | The complete environment inventory — the agent never invents a variable name absent from this file | ## The shipped skills Each file under `.agents/skills/` is a complete brief: paste it or let the agent read it, and the kit's rules travel with it — product UI stays in the web app, only OS behavior goes in Rust. The pasteable `prompts/` library ships only in the Web repository; this checkout carries skills instead. For an unattended setup, use the ready prompt in the agent-setup guide. [Let the agent run the setup](https://supastack.dev/docs/desktop/agent-setup.md) - `add-a-desktop-capability` — window, tray, shortcut, update or build changes - `adding-a-dependency` — add, upgrade or move an npm dependency safely - `reviewing-a-pr` — review a diff for correctness, workspace isolation and secrets - `upgrading-from-upstream` — pull a newer kit release into your shell - `verify-changes` — choose and run the right checks before handoff ## 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/desktop/src-tauri/src/lib.md` explains the decisions behind `apps/desktop/src-tauri/src/lib.rs` 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. ## 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/desktop/testing.md) - It ran the checks it claims — cargo test, cargo check or the release preflight — ask for the command and its output, not a conclusion. - Secrets stayed server-only — no signing key or certificate was printed into a report or commit, and the shell still carries no provider credentials. - Product behavior stayed in the web app — the diff adds no parallel UI, auth or chat logic in Rust. - The `.ai/` mirror of each changed file moved with the code. - Publishing, releasing installers and paid provider calls stayed separate steps you approved — the agent never merged, pushed or released on its own. - If a step failed twice, it stopped and reported the failing command instead of improvising around it. --- 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: [Menus, zoom, and startup](https://supastack.dev/docs/desktop/menus.md) # Menus, zoom, and startup The native menu bar, persisted zoom, launch at login, and tray behavior. ## The application menu The Supastack menu holds About, Check for Updates…, a Beta Updates checkbox (enabled only when an updater endpoint carries a channel slot), a Launch at Login checkbox, and Quit behind CmdOrCtrl+Q. The Edit menu keeps the standard undo, cut, copy, paste, and select-all items — required on macOS, where system shortcuts otherwise never reach the webview. ## Navigate, View, and Window Navigate offers Home (CmdOrCtrl+Shift+H), Back (CmdOrCtrl+[), Forward (CmdOrCtrl+]), and Reload (CmdOrCtrl+R); Home returns to the configured app URL. View offers Zoom In (CmdOrCtrl+=), Zoom Out (CmdOrCtrl+-), Actual Size (CmdOrCtrl+0), and Toggle Full Screen. Window offers Minimize and Close Window — closing still hides to the tray instead of quitting. ## Zoom persists per machine Zoom applies a CSS zoom factor to the page — the same mechanism across WKWebView, WebView2, and WebKitGTK — bounded between 0.3× and 3.0× in steps of 0.1. The choice persists in a zoom-state.json sidecar in the app config directory and mirrors into the page's per-origin storage, so it survives restarts and reloads alike. ## Launch at Login and the tray Launch at Login registers the app in the operating system's startup list, and the checkbox mirrors the real OS state rather than an assumed one. The tray icon menu offers Show Supastack and Quit; a left click toggles the window — hidden or minimized means show and focus, focused means hide. ## Verification and troubleshooting Walk each menu on the target OS: accelerators must fire inside the webview, zoom must survive a restart, the beta toggle must stay disabled without a channel-capable feed, and the tray left-click must summon then hide the window. --- 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, open `.agents/skills/` and copy the skill closest to your task — `add-a-desktop-capability` for shell behavior. 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/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/desktop/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 stays yours to provision, and the shipped release workflow plus updater feed are covered in Auto-updates. [Native acceptance checklist](https://supastack.dev/docs/desktop/run.md) [Auto-updates and release channels](https://supastack.dev/docs/desktop/updates.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/placeholder-icons from placeholder-icon.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, open `.agents/skills/` and copy the skill closest to your task — `add-a-desktop-capability` for shell behavior. 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/desktop/before-you-start.md) [Terminal basics](https://supastack.dev/docs/desktop/terminal-basics.md) [Desktop local setup](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) [Agent workflow & project memory](https://supastack.dev/docs/desktop/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 stays yours to provision, and the shipped release workflow plus updater feed are covered in Auto-updates. [Native acceptance checklist](https://supastack.dev/docs/desktop/run.md) [Auto-updates and release channels](https://supastack.dev/docs/desktop/updates.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: [Auto-updates and release channels](https://supastack.dev/docs/desktop/updates.md) # Auto-updates and release channels Signed update feeds, the stable/beta channel, and the shipped release workflow. ## Generate the updater keypair once The updater verifies minisign signatures on downloaded artifacts. Generate a keypair with the Tauri signer and keep the private key as TAURI_SIGNING_PRIVATE_KEY with its password in TAURI_SIGNING_PRIVATE_KEY_PASSWORD, both in your secret store — never in the repo. Builds that create updater artifacts emit a .sig sidecar next to each installer; the public key goes to clients, the private key stays in CI. ``` corepack pnpm --filter @supastack/desktop exec tauri signer generate -w ~/.tauri/product.key ``` ## Configure the updater at build time tauri.conf.json ships a sentinel updater block with empty endpoints and pubkey — keep it, because the updater plugin cannot initialize without the keys present. Real values are deep-merged at build time through a --config JSON overlay or the TAURI_CONFIG environment variable; check:release-config prints the exact overlay to paste when SUPASTACK_UPDATER_ENDPOINT and SUPASTACK_UPDATER_PUBKEY are set. Set both or neither: an endpoints-only overlay leaves the pubkey empty, so checks appear to work while every install fails signature verification. The endpoint URL carries {{target}}, {{arch}}, and {{current_version}} placeholders — the version slot is required — plus an optional {{channel}} path segment that enables release channels. ``` # check:release-config prints this overlay for tauri build --config: # {"plugins":{"updater":{"endpoints":["https://releases.example.com/{{channel}}/{{target}}/{{arch}}/{{current_version}}"],"pubkey":""}}} # SUPASTACK_UPDATER_ENDPOINT= # SUPASTACK_UPDATER_PUBKEY= ``` ## Publish the latest.json feed Each endpoint answers a latest.json manifest — version, notes, pub_date, and a platforms map covering darwin-aarch64, darwin-x86_64, windows-x86_64, windows-aarch64, linux-x86_64, and linux-aarch64 — where every entry pairs the installer URL with its signature. Build it with the release:manifest script: it scans a bundle directory for .app.tar.gz, -setup.exe (preferred over .msi), and .AppImage artifacts — each must have its .sig neighbour — and writes or merges the manifest. Flags cover --version, --base-url, --bundle-dir with --platform, --merge to accumulate platforms across runs, --notes, --pub-date, and --out. ``` corepack pnpm --filter @supastack/desktop release:manifest -- \ --version 1.2.0 \ --base-url https://releases.example.com/stable \ --bundle-dir src-tauri/target/release/bundle \ --platform windows-x86_64 \ --merge latest.json ``` ## Stable and beta channels When an endpoint contains the {{channel}} placeholder, the app menu enables a Beta Updates checkbox that switches the feed between stable and beta. The choice persists in an update-channel file next to the app config; SUPASTACK_UPDATE_CHANNEL overrides it per run — anything unrecognized is ignored with a warning — and stable is the default. Without the placeholder the toggle stays disabled and only the stable feed is checked. ## When updates are checked One silent background check runs at startup and only shows a dialog when an update exists; the Check for Updates… menu item reports every outcome. Checks serialize — a second concurrent request is dropped. An available update offers Install and Restart, which downloads, installs, and restarts the app; unconfigured builds answer that updates are not configured, and failures surface as dialogs rather than retries. ## The shipped release workflow .github/workflows/desktop-release.yml builds the matrix — macOS on both architectures, Windows, and Ubuntu — on desktop-v* tags or a manual dispatch with a version and channel. It stamps the manifest version from the tag (a release that skips this offers itself as an update forever), merges the per-platform manifests, and creates a draft GitHub release carrying latest.json plus every installer and signature — marked prerelease for beta channels. It needs SUPASTACK_APP_URL and SUPASTACK_UPDATER_ENDPOINT as repository variables and SUPASTACK_UPDATER_PUBKEY plus the signing private key and its password as secrets; Apple signing credentials are optional add-ons. The full runbook lives in `docs/desktop-updates.md` in the repository. ## Verification and troubleshooting Install a build, publish a newer manifest to a test feed, and confirm the update dialog arrives on the next check and the app restarts into the new version. Then switch to beta and confirm the channel feed answer changes. [Bundle and release](https://supastack.dev/docs/desktop/release.md) [Code signing](https://supastack.dev/docs/desktop/signing.md) --- Source: [Inside the desktop shell](https://supastack.dev/docs/desktop/internals.md) # Inside the desktop shell Boot page, navigation gate, plugin stack, and configuration reference. ## The boot page The window loads the bundled page in apps/desktop/ui first: it reads the injected __SUPASTACK_APP_URL__, probes the backend with a ten-second timeout, and forwards to it when reachable. An unreachable backend renders a Can't reach host state with a Retry that also fires on network recovery, so a cold start never shows a blank window. A ?target= query parameter exercises the page in a plain browser. ## Native features are plugins, not IPC The shell registers no custom commands — every native capability arrives through Tauri plugins: single-instance with the deep-link feature (which routes second-launch arguments on Windows and Linux), deep-link, window-state (geometry only — zoom and channel keep their own sidecars), opener, dialog, process (restart after update), updater, autostart, and global-shortcut, plus the tray-icon feature. Extending the shell means adding a plugin and a permission, not forking webview UI. ## The navigation gate Navigation stays for the bundled shell pages, the dev-server origin, and the exact configured app origin. Credential-free HTTP(S), mailto:, and tel: links leave through the system browser via the opener plugin; every other scheme is cancelled. A deep link arriving before the window exists is buffered in managed state and applied once it does. ## Config and capability reference tauri.conf.json sets productName, version (bump before a release — the workflow can override it), the identifier (check:release-config requires changing it from com.supastack.desktop), the supastack deep-link scheme, the updater sentinel block, bundle targets, and the placeholder icons. capabilities/default.json grants core:default plus opener:allow-open-url — widen it only when a new plugin needs more. ## How the shell is tested corepack pnpm --filter @supastack/desktop test syntax-checks the boot page and runs the Rust unit suite covering URL resolution, deep links, zoom, and channel logic; typecheck runs cargo check. The JavaScript release scripts are covered by the repository's vitest suites, and the CI pipeline builds real installers on all three operating systems for every change. ``` corepack pnpm --filter @supastack/desktop typecheck corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop check:release-config ``` ## Verification and troubleshooting Run the package suite after touching src-tauri, then verify the native behavior by hand on each target OS — unit checks prove the rules, not the window. Keep new shell behavior inside plugins and menu items so the webview stays a plain web app. --- Source: [Configure the desktop shell](https://supastack.dev/docs/desktop/configuration.md) # Configure the desktop shell Point the window at your app, set the summon shortcut and window preferences, and keep native settings out of the web bundle. ## Setup and configuration `SUPASTACK_APP_URL` chooses the Web app the shell loads. A runtime value wins over the build-time value, so the same installer can point at staging; production builds should compile an HTTPS origin. The window only navigates inside that origin — other links open in the default browser. The three window preferences also exist as menu checkboxes and persist in the app config directory. `SUPASTACK_DESKTOP_HOTKEY` sets the global summon shortcut, and the `SUPASTACK_DESKTOP_ALWAYS_ON_TOP`, `SUPASTACK_DESKTOP_START_MINIMIZED`, and `SUPASTACK_DESKTOP_CLOSE_TO_TRAY` booleans override the persisted choice for one run. Start Minimized applies at window creation; Close to Tray is re-read on every close. ``` SUPASTACK_APP_URL=https://app.example.com SUPASTACK_DESKTOP_HOTKEY=CommandOrControl+Shift+Space ``` ## Implementation workflow `tauri.conf.json` holds the product name, version, identifier, deep-link scheme, updater sentinel, and bundle targets. `capabilities/default.json` limits the webview to core services plus opening URLs. Never put provider secrets in the shell; it loads the same web app, and the server remains the trust boundary. ## Verification and troubleshooting Launch with a runtime `SUPASTACK_APP_URL` and confirm the window loads that origin. Toggle each window preference from the app menu, restart, and check that the persisted choice survives. Remove the environment overrides afterward so the menu state is authoritative again. --- Source: [Sign-in and sessions](https://supastack.dev/docs/desktop/session.md) # Sign-in and sessions Use the Web app's accounts and session rules inside the native window, including deep links that return to the app. ## Setup and configuration The desktop shell does not implement its own login. It opens the Web app in the system webview, and sign-in happens on that origin with the same session and workspace rules as a browser. The webview profile keeps cookies and local storage in the app's data directory between launches. The `supastack://` deep-link scheme maps links back to same-origin app routes. Invitations, password recovery, and other links must resolve to paths the app serves; links to another origin are ignored rather than opened inside the privileged window. Production builds must allow the scheme the server hands out. ## Implementation workflow Sign in, quit the app, and reopen it: the session should return without a second login. Sign out and repeat to confirm the session is gone. Test an expired session and an invite link, then clear the app data directory to reproduce a first launch. ## Verification and troubleshooting Authentication matches the Web app's behavior. --- Source: [Test the desktop shell](https://supastack.dev/docs/desktop/testing.md) # Test the desktop shell Run the Rust and boot-page suites, validate the release config, then verify the installed artifact on each target OS. ## Setup and configuration The desktop package has two layers of automated checks: the boot page is syntax-checked, and the Rust suite covers URL resolution, the navigation gate, deep links, updater channels, zoom persistence, and window preferences. Both run on your computer without a packaged installer, so start here before installing a build. ``` corepack pnpm --filter @supastack/desktop test corepack pnpm --filter @supastack/desktop typecheck node apps/desktop/scripts/check-release-config.mjs ``` ## Implementation workflow `check-release-config.mjs` validates the public app URL, the HTTPS updater endpoint with the version token, the public key, and the product identifier. Run it before every release build so a placeholder config fails locally instead of shipping. ## Verification and troubleshooting Install the built artifact on each target OS and verify the tray, the summon shortcut, Always on Top, Start Minimized, Close to Tray, an update check, the offline banner, and session persistence. Unit tests prove the rules; only the installed app proves the window. --- Source: [Troubleshooting the desktop app](https://supastack.dev/docs/desktop/troubleshooting.md) # Troubleshooting the desktop app Resolve missing toolchains, blank windows, and release-signing failures before they block a build. ## Setup and configuration On Windows install the C++ Build Tools with the Windows SDK and the WebView2 runtime. On macOS install the Xcode command-line tools. On Linux install the WebKitGTK, compiler, tray, and system packages listed for your distribution. Install Rust with rustup, reopen the terminal, and run `cargo check` before starting Tauri. A blank window usually means `SUPASTACK_APP_URL` is unreachable or points at a stopped dev server; the offline banner appears when the loaded page loses the network. Confirm the URL opens in a browser on the same machine and that you built with the intended value. ## Implementation workflow Release failures usually come from signing, notarization, or the updater block. Check the certificate and keychain on macOS, the signing certificate and timestamp server on Windows, the updater public key and endpoint path, and the product identifier. Re-run the release-config check, then install the artifact on a clean machine instead of over the development build. ## Verification and troubleshooting The app launches, reaches the backend, and installs cleanly. --- 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: [Auth module](https://supastack.dev/docs/modules/auth.md) # Auth module 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-email.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 `packages/web-core/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 packages/web-core/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 packages/web-core/src/job-handlers.ts and packages/web-core/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-21._ > **Which is better for cross-platform apps: supastack or TurboStarter?** > It depends on which clients and server capabilities your product needs. TurboStarter is the better fit if a browser extension is core to your product, or if you want Polar, Dodo Payments, and Superwall alongside Stripe; it ships web, mobile, and extension from one monorepo. supastack is the stronger choice for B2B or AI products that need team workspaces, workspace API keys, signed webhooks, an audit log, AI usage budgets, and a desktop app. Its 144-file test suite also makes long-term changes safer. Both are current, serious starters. ## 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: [Best SaaS Boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md) # Best SaaS Boilerplates in 2026 A comprehensive comparison of the best SaaS boilerplates and Next.js starter kits in 2026. Compare supastack, supastarter, ShipFast, Makerkit, TurboStarter, MkSaaS, and more on multi-tenancy, authentication, payments, AI, and pricing. _Published 2026-10-27._ > **What is the best SaaS boilerplate in 2026?** > It depends on your target platforms, team model, and budget. For most developers building a B2B, AI, or cross-platform SaaS in 2026, supastack is the strongest overall foundation: one TypeScript monorepo shipping Next.js, TanStack Start, Expo mobile, and Tauri desktop with Better Auth, multi-tenant workspaces, eight billing adapters, AI usage budgets, AGENTS.md guardrails, and 144+ automated test files. If you want Nuxt support, consider supastarter; for a weekend single-user MVP, ShipFast; for deep Supabase coupling, Makerkit; and for a browser extension, TurboStarter. ## Quick Comparison: Best SaaS Boilerplates in 2026 Building a production SaaS from scratch takes months of undifferentiated plumbing: authentication, webhook idempotency, multi-tenant role-based access control (RBAC), AI usage budgets, and cross-platform builds. A production-ready SaaS boilerplate gives you that foundation on day one so you can spend your time on the product features your customers actually pay for. Here is how the top SaaS starter kits and Next.js boilerplates compare in 2026. - **Best Overall (Web, Mobile, Desktop & AI):** **supastack** — Unified Turborepo monorepo with Next.js 16, TanStack Start, Expo mobile, Tauri 2 desktop, Better Auth, 8 payment providers, encrypted BYOK AI budgets, `AGENTS.md`, and 144+ test files. - **Best for Nuxt & Multi-Framework Web:** **supastarter** — Polished web-first boilerplate for Next.js and Nuxt with Better Auth, organization multi-tenancy, and five payment providers. - **Best for Dedicated Supabase Teams:** **Makerkit** — B2B SaaS starter kit built tightly around Supabase Auth, Postgres RLS, Next.js, and React Router. - **Best for Rapid Solo Indie MVPs:** **ShipFast** — Lightweight Next.js template built for solo founders validating a simple B2C idea over a weekend. - **Best for Browser Extension Products:** **TurboStarter** — Cross-platform monorepo pairing Next.js and Expo with a WXT browser extension. | Boilerplate | Frameworks & Surfaces | Auth & Multi-Tenancy | Payments & Billing | AI, Agents & Tests | License | |---|---|---|---|---|---| | supastack | Next.js 16, TanStack Start, Expo (iOS/Android), Tauri 2 desktop | Better Auth (2FA, passkeys, OAuth, RBAC), org workspaces, Auth.js option | 8 web providers (Stripe, Lemon Squeezy, Polar, Creem, Dodo, Paddle, Whop, PayPal) + RevenueCat | AI router, encrypted BYOK budgets, API keys, webhooks, AGENTS.md, 144+ test files | Lifetime (from $149) | | supastarter | Next.js, Nuxt | Better Auth (2FA, passkeys, orgs, RBAC) | Stripe, Lemon Squeezy, Polar, Creem, Dodo | AI helpers, AGENTS.md, Agent Skills | Lifetime | | Makerkit | Next.js, React Router (Remix) | Supabase Auth, full orgs and RBAC | Stripe, Lemon Squeezy, Paddle | AI add-ons, E2E test suite | Lifetime / annual | | ShipFast | Next.js | Auth.js (NextAuth), single-user focus | Stripe, Lemon Squeezy | Minimal (landing & MVP focus) | Lifetime | | TurboStarter | Next.js 16, Expo, WXT browser extension | Better Auth (orgs, 2FA, passkeys) | Stripe, Polar, Lemon Squeezy, Dodo, RevenueCat, Superwall | OpenAI demo, 15 test files | Lifetime | | MkSaaS | Next.js 16 | Better Auth (password, OAuth, admin) | Stripe, Creem, Waffo + credits system | AI SDK (OpenAI, DeepSeek), 18 test files | Lifetime | | Indie Kit Pro | Next.js 16 | Better Auth (B2B orgs sold separately) | Stripe, Polar, Paddle, Dodo, PayPal, Lemon Squeezy + credits | OpenAI & Replicate helpers, 0 test files | Lifetime | | ShipAny | Next.js 16 | Better Auth, admin RBAC | Stripe, Creem, PayPal + credits | OpenRouter, Replicate, fal, Gemini, 0 test files | Lifetime | | TanStarter | TanStack Start + Vite (Cloudflare) | Better Auth with admin | Stripe, Creem, Waffo | AI SDK + fal.ai, 20 test files | Lifetime | | SaaSBold | Next.js | Auth.js (social, magic link), no orgs | Stripe | OpenAI integration, Figma files | Lifetime | ## What to Look for in a SaaS Boilerplate in 2026 A SaaS starter kit is not a landing page theme — it is the security, billing, and data architecture your product will live inside for years. Before buying a Next.js or TypeScript boilerplate, evaluate these six load-bearing layers. **1. Modern Authentication & Zero Lock-In** — Look for first-class support for email OTP, magic links, OAuth, two-factor authentication (2FA), WebAuthn passkeys, session revocation, and admin impersonation via libraries like `Better Auth`. Avoid starters that lock your user table inside a proprietary third-party auth cloud where per-MAU pricing spikes as you grow. See our [authentication architecture guide](https://supastack.dev/blog/authentication-beyond-the-login-form.md) for what happens after the login screen. **2. Payment Flexibility & Merchant of Record (MoR) Support** — Global SaaS products often outgrow a single gateway. Choose a boilerplate that normalizes `Stripe` alongside Merchant of Record providers like `Lemon Squeezy`, `Polar`, `Paddle`, `Creem`, and `Dodo Payments` behind an idempotent webhook ledger — plus `RevenueCat` if you ship on iOS and Android. Read how [idempotent billing webhooks](https://supastack.dev/blog/idempotent-billing-webhooks.md) prevent duplicate fulfillment. **3. B2B Multi-Tenancy, RBAC & Database Isolation** — Selling to companies requires organization workspaces, team invitations, seat-based billing, and `owner`/`admin`/`member` role hierarchy. Every query must derive tenant scope on the server and pair with indexed `PostgreSQL` Row-Level Security (RLS) policies — see [Postgres RLS and workspace auth](https://supastack.dev/blog/postgres-rls-and-workspace-auth.md) and test your schema with the free [Supabase RLS Policy Generator](https://supastack.dev/tools/supabase-rls-generator). **4. Multi-Framework & Cross-Platform Reach** — In 2026, many SaaS products need more than a browser tab. Whether you prefer `Next.js 16 App Router` or `TanStack Start` on the web, having an `Expo` (React Native) mobile app and a `Tauri 2` desktop app sharing the same TypeScript contracts in one `Turborepo` monorepo saves months of API duplication. **5. AI Cost Controls & Coding Agent Readiness (`AGENTS.md`)** — If your SaaS includes AI features, raw API calls will wreck your margins; you need a model router, encrypted bring-your-own-key (BYOK) storage, per-workspace token budgets, and `Trigger.dev` background jobs. Equally important is how well the repo works with `Claude Code`, `Cursor`, and `Windsurf`: look for `AGENTS.md`, `.ai/` architecture mirrors, `llms.txt`, and structured [Agent Skills](https://supastack.dev/docs/skills/overview.md). **6. Automated Test Suite & Self-Hosting Options** — A boilerplate with zero tests turns every customization into a manual regression hunt. Demand a deterministic test suite that verifies auth, tenant isolation, and billing webhooks locally without cloud credentials, plus clean `Docker`, `Vercel`, and self-hosted Postgres deployment guides. ## 1. supastack — Best Overall SaaS Boilerplate in 2026 **Best for:** Developers and teams building serious B2B, AI, or cross-platform SaaS products who want one tested foundation across Web (`Next.js` + `TanStack Start`), Mobile (`Expo`), and Desktop (`Tauri`). **supastack** ranks #1 in 2026 because it solves the whole product lifecycle rather than just the marketing landing page. Inside a clean `pnpm` and `Turborepo` monorepo, you get two web engine options (`Next.js 16 App Router` and `TanStack Start`), a native `Expo` iOS and Android app, and a signed `Tauri 2` desktop app for macOS, Windows, and Linux — all sharing typed packages for `Better Auth`, `Drizzle ORM` + `PostgreSQL`, multi-tenant workspaces, and 13-locale internationalization. Where most starters stop at a basic Stripe checkout, supastack ships eight web payment adapters (`Stripe`, `Lemon Squeezy`, `Polar`, `Creem`, `Dodo Payments`, `Paddle`, `Whop`, and `PayPal`) behind an idempotent `webhook_event` ledger, plus `RevenueCat` mobile purchasing, workspace API keys, signed outbound webhooks, an audit log, GDPR data export, and an AI router with encrypted per-workspace BYOK keys and spend budgets. It is also built from the ground up for AI-assisted engineering with `AGENTS.md`, `.ai/` file mirrors, 26+ Agent Skills, and 144+ automated test files runnable via `pnpm verify:local`. Explore the full [technical stack documentation](https://supastack.dev/docs/web/tech-stack.md) or model your tiers with the free [SaaS Pricing Page Generator](https://supastack.dev/tools/saas-pricing-generator). - **Pros:** Four client surfaces (`Next.js`, `TanStack Start`, `Expo` mobile, `Tauri` desktop); 8 payment providers + `RevenueCat`; full B2B organizations, RBAC, API keys, signed webhooks, and audit logs; encrypted BYOK AI router with workspace budgets; `AGENTS.md` + 26 Agent Skills; 144+ automated test files; lifetime license starting at $149 (with $100 launch savings already included and 50% builder cashback). - **Cons:** Deeper architecture than single-folder weekend templates if you only need a one-page waitlist; browser extensions (WXT) are not included out of the box. ## 2. Best Multi-Framework & B2B Starters: supastarter, Makerkit & TurboStarter If your team has a specific framework requirement like `Nuxt`, `React Router`, or a companion browser extension, these three starters are the strongest alternatives to evaluate. **supastarter** — *Best for developers who need Nuxt alongside Next.js.* supastarter is a mature, well-documented web SaaS boilerplate supporting both `Next.js` and `Nuxt`. It integrates `Better Auth` (with 2FA, passkeys, and organizations), five payment providers (`Stripe`, `Lemon Squeezy`, `Polar`, `Creem`, `Dodo`), i18n, and `AGENTS.md` specifications. **Pros:** Strong Nuxt support, five billing providers, self-hosting docs, lifetime license. **Cons:** Web-only (no native Tauri desktop app in the core kit), fewer enterprise developer-platform primitives (like signed outbound customer webhooks and encrypted BYOK AI budget enforcement) than supastack. **Makerkit** — *Best for teams committed to the Supabase ecosystem.* Makerkit is a battle-tested B2B starter for `Next.js` and `React Router` (Remix) built deeply around `Supabase Auth`, Postgres RLS, and multi-tenant team workspaces. **Pros:** Deep Supabase conventions, strong B2B organization flows, thorough documentation. **Cons:** Tight coupling to Supabase conventions, no desktop client, and higher ongoing license tiers compared to one-time cross-platform kits. **TurboStarter** — *Best for products that need a browser extension.* TurboStarter ships a `pnpm` monorepo with `Next.js`, `Expo` mobile, and a `WXT` browser extension sharing `Better Auth` and `Drizzle`. **Pros:** Includes a Chrome/Firefox extension target plus `Stripe`, `Polar`, `Lemon Squeezy`, `Dodo`, `RevenueCat`, and `Superwall`. **Cons:** No desktop app, no workspace API keys or signed outbound webhooks, no BYOK AI budget router, and a smaller test suite (15 test files vs supastack's 144+). Read our [full supastack vs TurboStarter comparison](https://supastack.dev/blog/supastack-vs-turbostarter.md). ## 3. Best Indie & Rapid-Launch Starters: ShipFast, MkSaaS, Indie Kit & SaaSBold When your primary goal is launching a single-user web app or credit-based AI tool as quickly as possible, these Next.js templates optimize for day-one speed. **ShipFast** — *Best for solo indie hackers validating a B2C idea in a weekend.* Created by Marc Lou, ShipFast popularized the modern Next.js boilerplate movement with landing page sections, `NextAuth`, `Stripe` / `Lemon Squeezy`, and `MongoDB` or `Supabase`. **Pros:** Very fast to learn, large indie community, conversion-focused landing blocks. **Cons:** Built for single-user MVPs rather than multi-tenant B2B teams; lacks RBAC, passkeys, mobile/desktop apps, and automated test depth. **MkSaaS** — *Best for web-only credit-based AI tools and bilingual English/Chinese launches.* MkSaaS is a polished single-app `Next.js 16` starter featuring a built-in credit ledger, blog, docs, newsletter, and `Cloudflare Workers` deployment branches. **Pros:** Ready-made credit system, rich marketing blocks, Cloudflare support, proven by the author's live products. **Cons:** Web-only, no organization workspaces or seat billing, no customer API keys or outbound webhooks. Read our [full supastack vs MkSaaS comparison](https://supastack.dev/blog/supastack-vs-mksaas.md). **Indie Kit Pro & Indie Kit B2B** — *Best for lifetime-deal (AppSumo) redemption and six payment gateways on the web.* Indie Kit wires `Stripe`, `Polar`, `Paddle`, `Dodo`, `PayPal`, and `Lemon Squeezy` with credit expiry and `Inngest` background jobs. **Pros:** Six web payment providers and built-in LTD code redemption. **Cons:** Team workspaces require buying a separate B2B repository, no mobile or desktop apps, and zero automated test files in the reviewed repo. See [supastack vs Indie Kit Pro](https://supastack.dev/blog/supastack-vs-indie-kit.md) and [supastack vs Indie Kit B2B](https://supastack.dev/blog/supastack-vs-indie-kit-b2b.md). **SaaSBold** — *Best for budget-conscious solo devs who want Figma files.* SaaSBold covers `Next.js`, `Auth.js`, `Stripe`, and `Prisma` or `Drizzle` at an entry-level price with Figma design assets. **Pros:** Affordable one-time price, clean UI, Figma source included. **Cons:** No multi-tenancy or organizations, Stripe-only, Next.js-only, and basic internationalization. ## 4. Best Niche AI & Edge Starters: ShipAny, AnotherWrapper & TanStarter Specialized starters target narrower niches such as pre-built AI generation screens or ultra-low-cost Cloudflare Workers hosting. **ShipAny** — *Best for launching a credit-based AI image, video, or chat portal.* Popular in the indie AI community, ShipAny Template Two connects `OpenRouter`, `Replicate`, `fal`, and `Gemini` with credits, affiliate links, and `Stripe` / `Creem` / `PayPal`. **Pros:** Wide model catalog out of the box, built-in affiliate and ad widgets. **Cons:** No multi-tenant team workspaces, no encrypted BYOK workspace budgets, web-only, and 0 test files. Read our [full supastack vs ShipAny comparison](https://supastack.dev/blog/supastack-vs-shipany.md). **AnotherWrapper** — *Best for demoing standalone AI wrapper screens fast.* AnotherWrapper bundles eight finished Next.js AI demo apps (chat, image, video, voice, vision, audio) wired to seven model providers. **Pros:** Fastest path to a rebrandable AI demo screen. **Cons:** Thin B2B infrastructure — no team workspaces, no BYOK spend guardrails, no customer API keys or webhooks, and only 2 test files. Read our [full supastack vs AnotherWrapper comparison](https://supastack.dev/blog/supastack-vs-anotherwrapper.md). **TanStarter** — *Best for minimal TanStack Start apps on Cloudflare Workers.* TanStarter is a lightweight single-app starter running `TanStack Start`, `Vite`, `Better Auth`, and `Cloudflare R2`. **Pros:** Near-zero hosting cost on Cloudflare Workers and a small (~365-file) codebase. **Cons:** Web-only, no team workspaces or RBAC, no API keys or webhooks; note that **supastack** now also includes a full `TanStack Start` web surface alongside `Next.js`, `Expo`, and `Tauri`. Read our [full supastack vs TanStarter comparison](https://supastack.dev/blog/supastack-vs-tanstarter.md). ## Our Recommendation: Which SaaS Boilerplate Should You Choose? The right SaaS starter kit is the one whose architectural boundaries match the product you plan to run twelve months from now — not just the landing page you want to tweet on Friday. Use this decision matrix to match your project to the best boilerplate: For most founders and product teams in 2026, we recommend **supastack** because it combines `Next.js 16` and `TanStack Start` web engines, native `Expo` mobile, and `Tauri 2` desktop in one tested monorepo, backed by `Better Auth`, multi-tenant workspaces, eight payment providers, encrypted BYOK AI budgets, and `AGENTS.md` agent guardrails. If you are still weighing whether to adopt a boilerplate at all, read [starter kit or from scratch](https://supastack.dev/blog/starter-kit-or-from-scratch.md) and our [guide to choosing a SaaS stack](https://supastack.dev/blog/choosing-a-saas-stack.md). | You Are Building | Best Pick | Why It Wins | |---|---|---| | B2B, AI, or cross-platform SaaS (Web, Mobile, Desktop) | supastack | Unified Next.js + TanStack Start + Expo + Tauri monorepo, org RBAC, 8 payment providers, BYOK AI budgets, API keys, webhooks, 144+ test files | | Vue / Nuxt web SaaS | supastarter | First-class Nuxt support alongside Next.js with Better Auth and 5 payment providers | | Supabase-native B2B web app | Makerkit | Deep Supabase Auth and Postgres RLS conventions for Next.js and React Router | | Weekend single-user B2C MVP | ShipFast | Minimal Next.js setup optimized for rapid solo validation | | SaaS with a companion Chrome/Firefox extension | TurboStarter | Shared WXT browser extension package alongside Next.js and Expo | | Pay-per-credit web AI generator (EN/ZH) | MkSaaS / ShipAny | Pre-built credit ledgers and AI generation templates for single-user web tools | | Ultra-lean Cloudflare Workers web tool | TanStarter / supastack | TanStarter for a minimal single-app Worker; supastack for TanStack Start with full B2B and cross-platform packages | ## Frequently Asked Questions About SaaS Boilerplates Answers to the most common questions developers and technical founders ask before investing in a SaaS boilerplate in 2026. - Are SaaS boilerplates worth the investment in 2026? — Yes. Even with AI coding assistants like Claude Code and Cursor, wiring authentication, idempotent multi-provider billing webhooks, tenant isolation, background jobs, and mobile/desktop build pipelines takes 6 to 12 weeks to get right. A tested boilerplate recoups its one-time license cost in the first week of saved engineering time. - Why can’t I just ask Cursor or Claude Code to generate my boilerplate from scratch? — Coding agents generate UI screens quickly, but they hallucinate subtle security and billing bugs when asked to invent multi-tenant boundaries or webhook deduplication from a blank folder. Starting from a repository like supastack that already ships `AGENTS.md`, `.ai/` architecture contracts, 26 Agent Skills, and 144+ test files gives the agent verified guardrails to extend. - Can I customize everything and own the source code? — Yes. With supastack and most commercial starter kits, you receive full Git repository access with no proprietary runtime lock-in. Your product logic lives in clean modules that you can modify, self-host with Docker, or deploy to Vercel and Cloudflare. - What if I only need a web app right now — should I still pick a cross-platform boilerplate? — Yes. In supastack, the `Expo` mobile app and `Tauri` desktop app are optional workspaces in the monorepo that cost zero overhead until you need them, while the shared server packages (`Better Auth`, 8 billing adapters, workspaces, AI router, API keys, webhooks) are already more complete than web-only starters. - Which payment providers should a 2026 SaaS boilerplate support? — At minimum, look for `Stripe` plus a Merchant of Record (MoR) like `Lemon Squeezy`, `Polar`, `Paddle`, `Creem`, or `Dodo Payments` to handle global VAT and sales tax automatically, and `RevenueCat` if you plan to ship iOS or Android apps. supastack supports all eight web providers plus RevenueCat behind one idempotent webhook contract. - How do SaaS boilerplates handle updates without breaking my product code? — Good boilerplates separate core infrastructure packages from product domain code. supastack isolates auth, billing, database, and AI inside shared workspace packages and documents a step-by-step [update workflow](https://supastack.dev/docs/web/updating-the-kit.md) so you can pull upstream improvements cleanly. --- Source: [TanStack Start SaaS Boilerplate: What to Look For in 2026](https://supastack.dev/blog/tanstack-start-saas-boilerplate.md) # TanStack Start SaaS Boilerplate: What to Look For in 2026 What a TanStack Start SaaS boilerplate should include in 2026, and how supastack, supastarter and TanStarter compare on auth, teams, billing, hosting and price. _Published 2026-10-31._ > **What is the best TanStack Start SaaS boilerplate?** > It depends on what your product needs beyond the web app. For a lean, web-only tool on Cloudflare Workers, TanStarter keeps hosting costs lowest. supastarter suits teams that want Hono and oRPC with five payment providers. supastack fits B2B and AI products: its TanStack Start app costs $149 one-time and includes team workspaces, eight payment providers, API keys, signed webhooks and 13 languages. You can add Next.js, Expo mobile and Tauri desktop apps that share the same backend. ## The short answer A TanStack Start SaaS boilerplate saves you the weeks between `npm create` and your first paying customer: sign-in, teams, billing, emails, and a deployment that works. Three starters offer TanStack Start today, and they suit different products. - **Pick supastack** if you are building a B2B or AI product that needs team workspaces, API keys, webhooks and room to add mobile or desktop apps later. - **Pick supastarter** if you want Hono and oRPC for your API layer and already know the supastarter conventions from its Next.js edition. - **Pick TanStarter** if your product is a small web-only tool and near-zero hosting cost on Cloudflare Workers matters most. ## What a TanStack Start boilerplate should include TanStack Start gives you typed routes, route loaders, server functions and server rendering. It does not give you accounts, billing or tenant isolation. These are the parts to check before you buy any starter: | What to check | Why it matters | |---|---| | Server functions that check the session and workspace role | Typed routes do not protect data; every server function must confirm who is calling | | Payment webhooks on their own server routes | Providers retry deliveries, so each event must be verified and applied only once | | Teams from the first commit | Adding organizations later means changing every table and permission check | | A tested billing layer | Plan changes, seats and refunds are where generated code fails quietly | | A deployment path that already works | TanStack Start runs on many hosts, but the starter should ship one you can use today | | Written guides and agent instructions | TanStack Start has fewer examples online than Next.js, so docs fill the gap for you and your coding agent | ## TanStack Start boilerplates compared Here is how the three starters compare on the points above. The supastack column comes from its own source code; the others come from public sources listed below the table. | | supastack | supastarter | TanStarter | |---|---|---|---| | One-time price | $149 for TanStack Start | From $349 (Solo plan) | See vendor site | | Sign-in | Better Auth (Auth.js optional): passkeys, 2FA, magic link, OTP, OAuth | Better Auth: passkeys, 2FA, magic link, OAuth | Better Auth with admin | | Teams | Workspaces with invites and roles | Organizations with invites and roles | No | | Web billing | Stripe, Lemon Squeezy, Paddle, Polar, Creem, Dodo, Whop, PayPal | Stripe, Lemon Squeezy, Polar, Creem, Dodo | Stripe, Creem, Waffo | | Customer integrations | API keys, signed webhooks, audit log, OpenAPI | Typed API with OpenAPI | No | | Hosting | Node server or Docker | Vercel and other full-stack hosts | Cloudflare Workers | | Languages | 13 included | Translations included | en, zh | | Other apps that share the backend | Next.js, Expo mobile, Tauri desktop | Not part of this edition | No | ## What you get with supastack's TanStack Start app supastack's TanStack Start app is a full SaaS web app, not a demo. It sits on the same shared packages as the Next.js app, so sign-in, billing, the database and AI behave the same in both. Start with the [five-minute quickstart](https://supastack.dev/docs/tanstack/ship-in-5-minutes.md) to run it locally with fake providers and no accounts. - Sign-in with email codes, passwords, magic links, social accounts, passkeys and two-factor authentication. - Personal and team workspaces with invites, roles, per-seat billing and an admin area. - Billing through any of eight providers, with one webhook flow that ignores repeat deliveries. - An AI assistant with each workspace's own provider keys stored encrypted and spending kept within a budget. - A versioned API with workspace API keys, outbound webhooks, an audit log and an OpenAPI reference for your customers. - Background jobs, transactional email, sitemap and robots files, and 13 languages. - Over 580 automated test files across the repository, plus `AGENTS.md` and 27 Agent Skills that keep Cursor, Claude Code and Codex on the existing patterns. ## When another starter is the better fit supastack is not the smallest option. If you are testing one idea alone and want the lowest monthly bill, TanStarter's single Cloudflare Workers app has far less code to read. If your team already ships supastarter on Next.js, its TanStack Start edition keeps the same habits. If you are still choosing a framework, read [why TanStack Start suits SaaS](https://supastack.dev/blog/why-tanstack-start-for-saas.md) and the [Next.js vs TanStack Start comparison](https://supastack.dev/blog/nextjs-vs-tanstack-start-for-saas.md). For the wider market, see [the best SaaS boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md) and the full [supastack vs TanStarter comparison](https://supastack.dev/blog/supastack-vs-tanstarter.md). Sources: supastack source code as of October 2026; supastarter's TanStack Start product page (last updated August 13, 2026); TanStarter's source code as of September 2026. Features and prices change, so check each vendor's site before you buy. ## Common questions about TanStack Start boilerplates - Is TanStack Start ready for a paid SaaS product? — Yes for most teams. It is on stable 1.x releases, built on TanStack Router and Vite, and renders on the server. Its ecosystem is smaller than Next.js, so pick a starter with thorough guides. - Do I need Cloudflare to run TanStack Start? — No. supastack's TanStack Start app runs as a Node server and ships a Dockerfile for container hosts such as [Google Cloud Run](https://supastack.dev/docs/tanstack/deploy-google-cloud.md). TanStarter targets Cloudflare Workers instead. - Can I move between TanStack Start and Next.js later? — With supastack, both web apps share the same sign-in, billing, database and AI packages. Switching means porting screens and routes, not rewriting your backend. - Do I have to buy the Next.js app as well? — No. The TanStack Start app is $149 on its own. Any two platforms cost $298, and all four (Next.js, TanStack Start, Expo and Tauri) cost $299. See [pricing](/pricing). - Will coding agents handle a TanStack Start codebase? — They know Next.js better, so instructions matter more. supastack's `AGENTS.md` and Agent Skills show the agent where each change belongs. --- Source: [Next.js SaaS Boilerplate: What to Look For in 2026](https://supastack.dev/blog/nextjs-saas-boilerplate.md) # Next.js SaaS Boilerplate: What to Look For in 2026 What a Next.js SaaS boilerplate should include in 2026, and how supastack, supastarter, Makerkit and ShipFast compare on sign-in, teams, billing and price. _Published 2026-10-04._ > **What is the best Next.js SaaS boilerplate?** > It depends on who your customers are. ShipFast is the quickest start for a solo, single-user product. Makerkit suits teams committed to Supabase. supastarter fits teams that also want a Nuxt version. supastack fits B2B and AI products: its Next.js 16 app costs $149 one-time and includes team workspaces, eight payment providers, API keys, signed webhooks and 13 languages, with optional mobile, desktop and TanStack Start apps on the same backend. ## The short answer Next.js has the most SaaS boilerplates of any framework, so the hard part is not finding one but finding one that matches the product you will run a year from now. Four starters cover most needs. - **Pick supastack** if you are building a B2B or AI product that needs team workspaces, API keys, webhooks and room to add mobile or desktop apps later. - **Pick supastarter** if you want the same kit in Next.js and Nuxt. - **Pick Makerkit** if your team is committed to Supabase Auth and row-level security. - **Pick ShipFast** if you are validating a single-user idea alone and speed matters more than structure. ## What a Next.js boilerplate should include Next.js gives you routing, server components, server actions and a strong deployment story. It does not give you accounts, billing or tenant isolation. These are the parts to check before you buy any starter: | What to check | Why it matters | |---|---| | Server actions that check the session and workspace role | A server action is a public endpoint; each one must confirm who is calling | | Payment webhooks that ignore repeat deliveries | Providers retry events, and a second delivery must not grant access twice | | Teams from the first commit | Adding organizations later means changing every table and permission check | | Sign-in you own | A hosted auth service can price you per user as you grow; an open library keeps users in your database | | A real test suite | Without tests, every change to billing or sign-in becomes a manual check | | A way to take in upstream updates | Next.js moves fast, and the starter should explain how to pull its fixes into your code | ## Next.js boilerplates compared Here is how the four starters compare. The supastack column comes from its own source code; the others come from the public sources listed below the table. | | supastack | supastarter | Makerkit | ShipFast | |---|---|---|---|---| | License | $149 one-time for Next.js | From $349 one-time | Lifetime or annual | One-time | | Sign-in | Better Auth (Auth.js optional): passkeys, 2FA, magic link, OTP, OAuth | Better Auth: passkeys, 2FA, magic link, OAuth | Supabase Auth | Auth.js (NextAuth) | | Teams | Workspaces with invites and roles | Organizations with invites and roles | Organizations with roles | No | | Web billing | Stripe, Lemon Squeezy, Paddle, Polar, Creem, Dodo, Whop, PayPal | Stripe, Lemon Squeezy, Polar, Creem, Dodo | Stripe, Lemon Squeezy, Paddle | Stripe, Lemon Squeezy | | Other apps | TanStack Start, Expo mobile, Tauri desktop | Nuxt version | React Router version | No | ## What you get with supastack's Next.js app supastack's Next.js app runs on Next.js 16 with the App Router. It is the full product: the marketing site, the signed-in app and the admin area in one place. Start with the [five-minute quickstart](https://supastack.dev/docs/web/ship-in-5-minutes.md) to run it locally with fake providers and no accounts. - Sign-in with email codes, passwords, magic links, social accounts, passkeys and two-factor authentication. - Personal and team workspaces with invites, roles, per-seat billing and an admin area. - Billing through any of eight providers, with one webhook flow that ignores repeat deliveries. - An AI assistant with each workspace's own provider keys stored encrypted and spending kept within a budget. - A versioned API with workspace API keys, outbound webhooks, an audit log and an OpenAPI reference for your customers. - A marketing site, blog, docs, sitemap and 13 languages ready for search engines. - Over 580 automated test files across the repository, plus `AGENTS.md` and 27 Agent Skills for Cursor, Claude Code and Codex. ## When another starter is the better fit supastack has more structure than a one-page template. If you only need a landing page and a Stripe button to test an idea this weekend, ShipFast has less to learn. If your whole stack already runs on Supabase, Makerkit follows its conventions closely. If you need Vue, supastarter's Nuxt version is the stronger choice. Still choosing a framework? Read [why Next.js works well for SaaS](https://supastack.dev/blog/building-saas-with-nextjs.md) and the [Next.js vs TanStack Start comparison](https://supastack.dev/blog/nextjs-vs-tanstack-start-for-saas.md). For every starter we reviewed, see [the best SaaS boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md). Sources: supastack source code as of October 2026; supastarter's public site; our September 2026 reviews of Makerkit and ShipFast. Features and prices change, so check each vendor's site before you buy. ## Common questions about Next.js boilerplates - Is a boilerplate still worth it when I use an AI coding agent? — Yes. Agents write screens quickly but often get billing retries and tenant checks wrong. A tested starter gives them working patterns to copy. - Can I host a supastack Next.js app outside Vercel? — Yes. It runs on Vercel, in Docker or on your own server; the docs include a [Google Cloud Run guide](https://supastack.dev/docs/web/deploy-google-cloud.md). - Do I have to buy the mobile and desktop apps? — No. The Next.js app is $149 on its own. Any two platforms cost $298, and all four (Next.js, TanStack Start, Expo and Tauri) cost $299. See [pricing](/pricing). - How do I get updates without losing my changes? — supastack keeps shared code in packages and documents an [update workflow](https://supastack.dev/docs/web/updating-the-kit.md) for pulling in new releases. --- Source: [Expo SaaS Boilerplate: What to Look For in 2026](https://supastack.dev/blog/expo-saas-boilerplate.md) # Expo SaaS Boilerplate: What to Look For in 2026 What an Expo and React Native SaaS boilerplate should include in 2026, and how supastack, TurboStarter and Ignite compare on backend, sign-in, purchases and price. _Published 2026-10-09._ > **What is the best Expo boilerplate for a SaaS mobile app?** > It depends on whether you already have a backend. Ignite is a free, well-maintained React Native starter for teams that bring their own server. TurboStarter suits products that pair a mobile app with a browser extension. supastack fits B2B and AI products: its Expo app shares sign-in, workspaces, billing and AI with a supastack web backend, and adds RevenueCat purchases, push notifications, offline access and app lock. The Expo app costs $149 one-time, or $298 together with a web app. ## The short answer A SaaS mobile app is rarely just screens. It needs the same accounts, teams and subscriptions as your web app, plus store purchases and push notifications. The right Expo starter depends on how much of that backend you want included. - **Pick supastack** if your mobile app is part of a SaaS product and should share sign-in, workspaces, billing and AI with the web. - **Pick TurboStarter** if a browser extension is part of your product alongside web and mobile. - **Pick Ignite** if you only need a solid React Native app structure and already have a backend. ## What an Expo boilerplate should include Expo handles the native build, over-the-air tooling and store submission. It does not give you a backend. These are the parts to check before you buy any starter: | What to check | Why it matters | |---|---| | One backend shared with the web app | Two separate backends drift apart and double your security work | | Tokens kept in secure device storage | A stolen session token is as good as a password | | Store purchases tied to the same plans as the web | A customer who pays on iPhone must get the same access in the browser | | Push notifications and deep links | Invites and alerts must open the right screen in the app | | Offline behavior | Phones lose signal; the app should show recent data instead of a blank screen | | Build and submit profiles | Store release is where many first launches stall | ## Expo boilerplates compared Here is how the three starters compare. The supastack column comes from its own source code; the others come from the public sources listed below the table. | | supastack | TurboStarter | Ignite | |---|---|---|---| | License | $149 one-time for Expo | One-time | Free and open source | | Expo version | Expo 57 | Expo 57 | Expo | | Backend | supastack web app (Next.js or TanStack Start) | Shared with its web app | Bring your own | | Teams | Workspaces with invites and roles | Organizations with invitations | No | | Store purchases | RevenueCat | RevenueCat, Superwall | No | | Other apps | Next.js, TanStack Start, Tauri desktop | Next.js, browser extension | No | ## What you get with supastack's Expo app supastack's mobile app runs on Expo 57, React Native 0.86 and Expo Router for iOS and Android. It talks to the same server as the web app, so a customer's account, workspace and plan follow them across devices. Start with the [five-minute quickstart](https://supastack.dev/docs/mobile/ship-in-5-minutes.md). - Sign-in with email codes, social accounts and [passkeys](https://supastack.dev/docs/mobile/passkeys.md), with sessions kept in secure device storage. - Workspace switching, invites and roles that match the web app. - A paywall backed by [RevenueCat](https://supastack.dev/docs/mobile/revenuecat.md), with access checked against the same plans as web billing. - Push notifications, live updates and deep links that open the right screen. - An [offline cache](https://supastack.dev/docs/mobile/offline.md) for recent data and an optional [app lock](https://supastack.dev/docs/mobile/security.md) with Face ID, Touch ID or a device passcode. - An AI assistant and chat that use the workspace's own provider keys and budgets. - Build profiles for development, preview and production, plus a [publishing guide](https://supastack.dev/docs/mobile/publishing.md) for the App Store and Google Play. ## When another starter is the better fit If your backend already exists, or you are building a mobile-only app with no web product, Ignite gives you a mature React Native structure for free. If a browser extension is core to your product, TurboStarter ships one that shares code with its web and mobile apps. Still deciding on Expo itself? Read [why Expo is a practical way to ship a SaaS mobile app](https://supastack.dev/blog/building-saas-mobile-apps-with-expo.md) and the full [supastack vs TurboStarter comparison](https://supastack.dev/blog/supastack-vs-turbostarter.md). Sources: supastack source code as of October 2026; our September 2026 review of TurboStarter; Ignite's public GitHub repository. Features and prices change, so check each project before you buy. ## Common questions about Expo boilerplates - Does the supastack mobile app work without the web app? — No. It connects to a supastack web backend for sign-in, workspaces and billing. A Mobile-only purchase contains the Expo app but not that server, so most buyers pair it with Next.js or TanStack Start. - Do I need RevenueCat? — Only if you sell subscriptions inside the app. Apple and Google generally require their own payment systems for digital goods, and RevenueCat connects those purchases to your plans. - Can I test on a real phone before paying for developer accounts? — Yes. A development build runs on your own device; you need Apple and Google developer accounts only to publish. - How much does the Expo app cost? — $149 on its own, or $298 with a web app. Any two platforms cost $298, and all four (Next.js, TanStack Start, Expo and Tauri) cost $299. See [pricing](/pricing). --- Source: [Tauri Desktop App Boilerplate: What to Look For in 2026](https://supastack.dev/blog/tauri-desktop-app-boilerplate.md) # Tauri Desktop App Boilerplate: What to Look For in 2026 What a Tauri desktop app boilerplate should include in 2026, and how supastack, create-tauri-app and Electron React Boilerplate compare on accounts, updates and price. _Published 2026-10-28._ > **What is the best Tauri boilerplate for a SaaS desktop app?** > It depends on whether the desktop app belongs to a larger product. create-tauri-app is the free, official way to start an empty Tauri project. Electron React Boilerplate is a free option if you prefer Electron. supastack fits SaaS products: its Tauri 2 app costs $149 one-time and opens your supastack web app in a native window, with a tray icon, global shortcut, deep links, launch at startup and an auto-updater already wired in. ## The short answer Most SaaS desktop apps are your web app in a native window, plus the things a browser tab cannot do: a tray icon, a keyboard shortcut, links that open the app, and updates. Tauri does this with a small installer because it uses the operating system's webview instead of bundling Chromium. - **Pick supastack** if the desktop app is part of a SaaS product and should share accounts, workspaces and billing with your web app. - **Pick create-tauri-app** if you want an empty Tauri project and will build every native feature yourself. - **Pick Electron React Boilerplate** if you need Electron's bundled Chromium and Node.js APIs. ## What a Tauri boilerplate should include Tauri gives you the window, the Rust core and a plugin system. A desktop SaaS app needs more before it can ship. These are the parts to check: | What to check | Why it matters | |---|---| | Accounts and billing shared with the web app | Customers expect one login and one subscription on every device | | A narrow bridge between the web page and the system | Each native command the page can call is a door that must be guarded | | Auto-updates | Desktop users rarely reinstall; fixes only reach them through an updater | | Code signing guides | Unsigned apps trigger warnings on macOS and Windows that scare customers away | | Tray, shortcuts and deep links | These are the reasons people install a desktop app instead of keeping a tab open | ## Desktop boilerplates compared Here is how the three options compare. The supastack column comes from its own source code; the others come from their public repositories. | | supastack | create-tauri-app | Electron React Boilerplate | |---|---|---|---| | License | $149 one-time for Tauri | Free and open source | Free and open source | | Runtime | Tauri 2, system webview | Tauri 2, system webview | Electron, bundled Chromium | | Accounts, teams and billing | Yes, from your supastack web app | No | No | | Native features | Tray, global shortcut, deep links, launch at startup, auto-updater | Add plugins yourself | Wire up Electron APIs yourself | | Release guides | Signing and update guides | Tauri documentation | Project documentation | ## What you get with supastack's Tauri app supastack's desktop app is built on Tauri 2 for macOS and Windows. It opens your supastack web app in a native window, so sign-in, workspaces, billing and AI work exactly as they do in the browser, and every web release reaches desktop users at once. Start with the [five-minute quickstart](https://supastack.dev/docs/desktop/ship-in-5-minutes.md). - A tray icon, with the option to hide to the tray when the window closes. - A global keyboard shortcut that brings the app to the front from anywhere. - Deep links that open the app on the right page, and a single running copy at a time. - Launch at startup, remembered window size and position, and native file dialogs. - An auto-updater ready to point at your release server, with [signing](https://supastack.dev/docs/desktop/signing.md) and [update](https://supastack.dev/docs/desktop/updates.md) guides. - [Menus](https://supastack.dev/docs/desktop/menus.md) and native commands kept behind a small, explicit bridge. ## When another option is the better fit supastack's desktop app assumes you have a web product. If you are building a fully offline tool, or an app that must work without any server, start from create-tauri-app and design your own data layer. If you depend on Chromium-only features or Node.js modules in the app itself, Electron is the safer choice. Still comparing desktop frameworks? Read [Tauri vs Electron](https://supastack.dev/blog/tauri-vs-electron-for-desktop-apps.md) and [why Tauri is a strong foundation for desktop apps](https://supastack.dev/blog/building-desktop-apps-with-tauri.md). Sources: supastack source code as of October 2026; the public create-tauri-app and Electron React Boilerplate repositories. Features change, so check each project before you choose. ## Common questions about Tauri boilerplates - Do I need to know Rust? — Not for most changes. supastack's desktop features are already written; you touch Rust only to add a new native command. - Does the desktop app work offline? — It loads your web app, so it needs a connection. Choose a different design if offline use is essential. - Can I sell the desktop app separately? — Yes. It is your product; you decide whether desktop is included in a plan or sold on its own. - How much does the Tauri app cost? — $149 on its own. Because it opens your web app, most buyers pair it with Next.js or TanStack Start: any two platforms cost $298, and all four cost $299. See [pricing](/pricing). --- 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._ > **Which is better for a web-only AI tool: supastack or MkSaaS?** > It depends on the product and its audience. MkSaaS is the faster path to a web-only, credit-based AI tool, particularly for a Chinese-market audience, with a simpler surface to learn. Choose supastack when the product must serve teams, add mobile or desktop clients, or integrate with customers' systems. supastack also ships workspace roles, API keys, signed webhooks, an audit log, and a substantially larger test suite, which matters as billing and permissions grow. If you only need a single-user credit tool and speed is everything, MkSaaS is reasonable; otherwise supastack is the sturdier foundation. ## 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-24._ > **Which is better for launching an AI SaaS: supastack or ShipAny?** > It depends on how far the product must go. ShipAny is a fast launcher for credit-based AI generation sites, with polished payment and generation flows for a single-user tool. supastack is the sturdier foundation when your AI SaaS serves teams, connects to other systems, or must change safely over time: team workspaces, encrypted bring-your-own keys, per-workspace budgets, API keys, signed webhooks, audit logs, and desktop alongside web and mobile. ShipAny wins on time to first demo; supastack wins on the infrastructure behind a durable B2B or multi-platform product. ## 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-25._ > **Which is better for monetizing a web app: supastack or Indie Kit Pro?** > It depends on how much product you intend to build. Indie Kit Pro is excellent for getting paid quickly on the web, with checkout and a lean surface that suits a focused single-user tool. supastack costs more and asks more of you up front, but it adds team workspaces, web, mobile, and desktop clients, API keys, webhooks, an audit log, AI budgets, and a large test suite. For a quick paid launch, Indie Kit Pro is the pragmatic choice. If the product will serve teams or grow across platforms, supastack's foundation saves rebuilding later. ## 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-26._ > **Which is better for a B2B SaaS: supastack or Indie Kit B2B?** > It depends on which B2B capabilities you need first. Indie Kit B2B covers the basics of team billing and offers more payment choices, so it can be the faster route for a conventional seat-based product. supastack goes further on identity and operations: modern auth with passkeys and 2FA, workspace roles, API keys, signed webhooks, an audit log, GDPR export, AI budgets, and a test suite. Choose Indie Kit B2B to launch a simpler billing-first product sooner; choose supastack when security, integrations, and long-term maintainability are central. ## 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-28._ > **Which is better for a low-cost SaaS: supastack or TanStarter?** > It depends on what dominates your costs. TanStarter is a smart choice for lean, web-only products where hosting cost dominates, using a lightweight TanStack Start app on Cloudflare Workers with a lower upfront price. supastack asks more upfront but bundles team workspaces, web, mobile, and desktop clients, background jobs, API keys, signed webhooks, an audit log, and AI usage controls in one tested codebase. If you are validating a small single-user tool, TanStarter lowers the risk. Once the product needs teams, multiple surfaces, or integrations, supastack is the better long-term trade. ## 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-30._ > **Which is better for launching an AI app: supastack or AnotherWrapper?** > It depends on whether the AI screen is the product or just one feature. AnotherWrapper gets an AI tool on screen fastest, with ready-made generation interfaces and integrations, which suits rapid validation. As general-purpose AI assistants make standalone screens easier to copy, the durable value shifts to infrastructure: team workspaces, encrypted provider keys, usage budgets, API keys, webhooks, audit logs, and a tested codebase, where supastack is stronger. Pick AnotherWrapper to test an idea this week; pick supastack when the business depends on cost control, teams, and integrations. ## 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: [Why TanStack Start is an exciting foundation for SaaS](https://supastack.dev/blog/why-tanstack-start-for-saas.md) # Why TanStack Start is an exciting foundation for SaaS Explore type-safe routing, server functions, and deployment choices with TanStack Start. _Published 2026-10-24._ > **Why use TanStack Start for a SaaS application?** > It depends on your team, but TanStack Start is compelling when typed routes, validated search parameters, route loaders, and explicit server functions matter. It combines TanStack Router with server rendering and streaming. supastack offers it alongside Next.js. ## A router that understands your application Workspace navigation, search filters, and pagination rely on URLs. TanStack Router brings typed routes, parameters, validated search state, and loaders into one model. Changes become easier to trace across the application. A valid workspace ID is still only a reference. Check the signed-in member and their permissions on the server before returning private data. ## An explicit server boundary TanStack Start adds server rendering, streaming, and server functions to the router. Validate inputs and authorize the caller inside trusted server code, and keep provider secrets there. External clients and webhooks need their own server routes and request verification. ## Where TanStack Start fits Choose the application model your team can maintain. Next.js is also a strong option for teams that prefer its App Router and React Server Components. | Need | TanStack Start | Why it matters | |---|---|---| | Navigation | Typed routes and search | Safer workspace links and filters | | Server work | Server functions and routes | Explicit trusted boundaries | | Delivery | Multiple deployment targets | Choose a compatible host | ## Start with the essentials supastack offers a TanStack Start web app with authentication, billing, workspaces, and AI features. Pick the framework that suits your product and use the existing comparison to evaluate both web options. - [TanStack Start documentation](https://tanstack.com/start/latest/docs/framework/react/overview) - [Next.js versus TanStack Start](https://supastack.dev/blog/nextjs-vs-tanstack-start-for-saas) - [TanStack Start SaaS boilerplate guide](https://supastack.dev/blog/tanstack-start-saas-boilerplate) --- Source: [Why Expo is a practical way to ship a SaaS mobile app](https://supastack.dev/blog/building-saas-mobile-apps-with-expo.md) # Why Expo is a practical way to ship a SaaS mobile app Build, test, and release iOS and Android SaaS apps with Expo and React Native. _Published 2026-10-23._ > **Why use Expo for a SaaS mobile app?** > It depends on your native requirements, but Expo gives React Native teams a productive workflow and access to native capabilities. Expo Router supports navigation, while EAS offers build, submission, and update services. Test on real devices and plan for app-store requirements. ## Build for the way people use a phone A mobile SaaS app should do more than shrink a website. Design for small screens, intermittent connections, platform navigation, and the tasks people perform on the move. Expo builds on React Native and offers tools for iOS and Android development. Start with one valuable journey, such as reviewing a workspace or responding to a notification, and test it on actual devices. ## Navigation and native boundaries Expo Router supports file-based navigation and deep linking. Your application still needs a deliberate sign-in flow: restore sessions safely, scope requests to the active workspace, and handle expired access clearly. Store sensitive values using appropriate secure storage. ## From development to release EAS Build produces installable binaries and EAS Submit helps upload builds to app stores. Your team still owns signing credentials, store accounts, privacy disclosures, and review. Eligible non-native changes can be delivered with EAS Update; native changes require a compatible new build. | Stage | What to verify | |---|---| | Development | Navigation, authentication, and errors | | Preview | Permissions, network loss, and deep links | | Release | Signing, store listings, and review | ## Start from a shared foundation supastack includes an Expo mobile app alongside its web and desktop options. Configure the app and backend using the mobile guide, then verify your own journeys on iOS and Android. - [Expo development overview](https://docs.expo.dev/workflow/overview/) - [EAS Build documentation](https://docs.expo.dev/build/setup/) - [Expo SaaS boilerplate guide](https://supastack.dev/blog/expo-saas-boilerplate) --- Source: [Why Tauri is a strong foundation for desktop apps](https://supastack.dev/blog/building-desktop-apps-with-tauri.md) # Why Tauri is a strong foundation for desktop apps Understand Tauri's webview and Rust architecture, native integration, and release considerations. _Published 2026-10-22._ > **Why use Tauri for a desktop application?** > It depends on the application, but Tauri runs a web frontend in a desktop shell backed by Rust and the operating system webview. It suits teams that want native integration without bundling a full browser runtime. Test platform differences, permissions, and release signing for your app. ## A web interface with a native boundary Tauri combines a web frontend with a Rust-based core and the operating system webview. Use familiar web tools for the interface while exposing native functionality through deliberate commands and permissions. Webviews differ across platforms. Test rendering, file access, and integration on every operating system you support. ## Design the bridge deliberately Treat every frontend-accessible command as an API. Validate inputs and paths, limit capabilities, and keep privileged operations behind a narrow boundary. A desktop shell does not make untrusted content safe. ## Plan for distribution Plan for platform signing, installers, update channels, and recovery when an update fails. Requirements vary by operating system and distribution channel, so verify the actual package on target machines. | Concern | Practical check | |---|---| | Webview | Test each supported platform | | Native access | Limit and validate commands | | Distribution | Verify signing, installation, and updates | ## A desktop starting point supastack includes a Tauri desktop surface built around the shared application. Its desktop guides cover setup and release preparation, leaving you to build the native workflows that distinguish your product. - [Tauri architecture](https://v2.tauri.app/concept/architecture/) - [Tauri security](https://v2.tauri.app/security/) - [Tauri desktop boilerplate guide](https://supastack.dev/blog/tauri-desktop-app-boilerplate) --- Source: [Tauri vs Electron: choosing a desktop framework](https://supastack.dev/blog/tauri-vs-electron-for-desktop-apps.md) # Tauri vs Electron: choosing a desktop framework Compare Tauri and Electron on runtime architecture, native integration, distribution, and team trade-offs. _Published 2026-10-21._ > **Should I choose Tauri or Electron for a desktop app?** > It depends on your priorities. Tauri uses the system webview with a Rust-based core, while Electron packages Chromium and Node.js around a multi-process model. Choose based on native integration, supported platforms, team skills, and testing needs; measure your own application. ## Different runtimes, different trade-offs Tauri hosts a web interface in the operating system webview and bridges it to a Rust core. Electron uses Chromium and Node.js and separates application responsibilities across processes. Both can deliver desktop software with web technologies; neither removes the need for safe boundaries. ## Compare the decisions that matter Look beyond headline download sizes. Dependencies, bundled assets, platforms, and update systems affect the result. Build and measure a representative app on the machines your customers use. | Decision | Tauri | Electron | |---|---|---| | Rendering | Operating system webview | Bundled Chromium | | Native layer | Rust commands and capabilities | Node.js and Electron APIs | | Testing focus | Webview differences | Process isolation and runtime | | Team fit | Web and Rust | Web and Node.js | ## Security and native integration Keep privileged operations behind a narrow interface, validate inputs, and limit access to local resources. Follow Electron's process isolation guidance or Tauri's capability model, and handle external content carefully in either framework. ## Choose for your product Choose Tauri when its system webview and Rust integration fit your team and you can test platform differences. Choose Electron when a consistent Chromium runtime and the Node.js ecosystem are decisive. Prototype your hardest native feature before committing. supastack offers a Tauri desktop option and a documented release path. Test signing, updating, and permissions for your own distribution targets. - [Tauri architecture](https://v2.tauri.app/concept/architecture/) - [Electron process model](https://www.electronjs.org/docs/latest/tutorial/process-model) - [Electron security](https://www.electronjs.org/docs/latest/tutorial/security) --- Source: [Why Next.js works well for a SaaS application](https://supastack.dev/blog/building-saas-with-nextjs.md) # Why Next.js works well for a SaaS application Explore the Next.js App Router, server components, and the practical decisions behind a production SaaS app. _Published 2026-10-20._ > **Why use Next.js for a SaaS application?** > It depends on your team, but Next.js offers file-based routing, server rendering, React Server Components, and a broad ecosystem. Its App Router helps compose server and client work in one application. You still need to design authentication, authorization, billing, and deployment for your product. ## Build around routes and layouts The App Router organizes pages and layouts in a file-based route tree. Shared layouts help keep navigation and workspace context consistent, while loading and error boundaries let each route describe its own states. A SaaS product still needs server-side authorization. Treat every workspace identifier as a request to be checked against the signed-in user. ## Choose where work runs Pages and layouts are Server Components by default. Fetch data and use secrets on the server, then use Client Components for interactions and browser APIs. Keep client boundaries focused and send only the data the interface needs. ## Plan the production path A framework handles routing and rendering, but your application still owns payment correctness, access control, data retention, and operational recovery. | Area | What to check | |---|---| | Identity | Sessions and workspace permissions | | Billing | Verified webhooks and replay handling | | Delivery | Deployment, logs, and recovery | ## Start from a working foundation supastack provides a Next.js web option with authentication, workspaces, billing, and AI features. Review the setup guide and compare it with the TanStack Start option before choosing the model that fits your team. - [Next.js App Router documentation](https://nextjs.org/docs/app) - [Next.js versus TanStack Start](https://supastack.dev/blog/nextjs-vs-tanstack-start-for-saas) - [Next.js SaaS boilerplate guide](https://supastack.dev/blog/nextjs-saas-boilerplate) --- Source: [Your stack list won’t ship your product](https://supastack.dev/blog/stack-lists-dont-ship-products.md) # Your stack list won’t ship your product A hundred developers bookmarked my stack list — what the list leaves out is the plumbing that let a Windows desktop tool ship in 48 hours. _Published 2026-10-12._ > **Does a longer tech stack list help you ship faster?** > It depends on what the list leaves out. A stack list names tools; it does not install their boundaries — verified webhooks, tenant scoping, release signing — and those seams are where projects stall. A copied list reproduces the tools with none of the integration. Lists work as a menu of things to research, but the path to a shippable product is tested plumbing, however you acquire it. The honest question is what your evenings are worth. ## The bookmark trap A few days ago I answered a thread on X with the stack I actually run — my “2026 indie dev stack,” a dozen tools in one reply. [The reply](https://x.com/noah_z_zhang/status/2114919719538978934) collected over a hundred bookmarks and sent more than a hundred clicks to this site. The analytics were less flattering: most visitors scanned a page and left. A stack list reads like a finished plan; a copied list ships nothing. Every tool still has to be installed, wired to its neighbors, and tested — the seams between them are where the weeks go. This is the post the bookmarks were looking for: not which tools I run, but which plumbing was already load-bearing — and what that let me ship in forty-eight hours. ## Where projects actually die Most indie projects do not die on their core feature. They die in week one, on infrastructure that has nothing to do with the idea. Motivation has a half-life of about forty-eight hours — every hour spent away from the product burns the scarcest resource a solo build has. The shape of the week is remarkably consistent: - Day one: monorepo layout, ESLint, `tsconfig` paths, database migrations. - Day three: still in the payment provider’s docs, getting webhook idempotency right. - Day five: a desktop or mobile companion — a new build pipeline, a new auth surface, auto-updates from scratch. ## A 48-hour build, end to end Last week I shipped [SpinShare](https://spinshare.dev), a Windows-native desktop utility: drag a file in Explorer, tap Shift, and a radial menu opens under the cursor; release to run a local conversion or operation. No popups, nothing uploaded; the file never leaves the machine. The timeline is the part worth examining: new repository, Rust core logic, an offline license flow on my existing billing rails, and a Microsoft Store submission — forty-eight hours of core development, pair-coding with an agent and almost no prompt-tuning. The agent did not derail for an unglamorous reason: the architecture boundaries where it hallucinates were already sealed inside supastack, my own kit. What follows is the accounting: the plumbing that did not have to be built in those two days. ## Setup and guardrails `pnpm setup` collapses bootstrap into one command: preflight checks, environment-variable alignment, dependency installs, database migrations. There is no first evening spent assembling a working environment — the repository either passes its own checklist or names what is missing. The `.ai/` mirror docs, `AGENTS.md`, and `.agents/skills/` are a contract written for the agent rather than the human — each package’s responsibility boundary, its test rules, its red lines (the desktop app is a single-webview shell that never forks product UI). The [project structure guide](https://supastack.dev/docs/web/project-structure.md) shows the layout it reads; the [agent workflow post](https://supastack.dev/blog/building-saas-with-a-coding-agent.md) covers the working rhythm. Boundaries that are written down are boundaries an agent cannot wander across. ## Billing and releases shipped ready-made Eight billing adapters sit behind one contract — Stripe, Creem, Polar, Dodo, Lemon Squeezy, Paddle, Whop, and PayPal — and a `webhook_event` ledger makes every settlement signature-verified and idempotent before it becomes a license or an email. My work began downstream of it: Early-Bird tiers that switch by order count, and Ed25519-signed offline license keys — a desktop tool must activate without a network call. The [billing module guide](https://supastack.dev/docs/modules/billing.md) documents the flow I extended. The release side was equally uninteresting, which is the point. The Tauri 2 pipeline — `desktop-release.yml` — builds signed bundles and generates `latest.json` via minisign for stable and beta channels across Windows (NSIS and msi), macOS (dmg), and Linux (AppImage). The only packaging work I did myself was an MSIX build for the Microsoft Store; the [desktop quickstart](https://supastack.dev/docs/desktop/ship-in-5-minutes.md) covers the rest. ## The ten percent users actually feel Here is the arithmetic a stack list hides. Roughly ninety percent of what a product needs is plumbing the user never sees. Invisible plumbing is the work a foundation should carry — it is the part that can be carried. The rest is the product. Because mine was already load-bearing, nearly all of my forty-eight hours went to the ten percent users can feel — work that never fits in a stack comment, and never happens while the week goes to webhooks and signing: - Windows OLE drag hooks in Rust, so the menu arms only while a real file drag is held with Shift — never while typing. - A multi-window tray flow, so the tool stays one keystroke away without a taskbar window. - The radial-menu animation, tuned until the menu reads instant instead of animated. - More than thirty local format converters, each with its own unit tests. ## The practical part: pricing and transparency The fair objection: “I have Cursor — why pay for a stack list?” It holds until your own time has a price. Two evenings of webhook idempotency cost you a feature you did not ship. On transparency: the [documentation](https://supastack.dev/docs/web/ship-in-5-minutes.md) is public in full — directory structure, dual web engines, cross-platform sync, all eight billing adapters, AI routing. I would rather a skeptic read it end to end and walk away than buy on a headline; [starter kit or from scratch](https://supastack.dev/blog/starter-kit-or-from-scratch.md) is the honest version of the decision. Two practical notes: the displayed prices already include $100 in launch savings, and checkout supports Alipay. And a 50% builder cashback — ship a real product within sixty days, share the build story publicly (5,000 organic views or 50 organic likes on X, Reddit, YouTube, or similar), and email me the link: half comes back, and the license stays yours. The rules live at [the launch-cashback terms](https://supastack.dev/terms#launch-cashback). ## Common questions about stack lists The same objections surface whenever the kit does. They deserve straight answers. - Can’t an agent scaffold all of this for me? — It can produce something plausible; it cannot verify the seams in one pass. An idempotent webhook is tested, not generated — scaffolding hands you the same list again. - Doesn’t a boilerplate make products generic? — The plumbing is shared; the product is not. SpinShare’s drag hooks, tray behavior, and converters exist nowhere in the kit — the ten percent customers see stays yours. - What if I only need a web app? — The kit is the purchase either way; mobile and desktop are surfaces to adopt or ignore, not requirements. SpinShare exists because the desktop surface was already built. - Is a desktop utility really the buyer story? — It is the shortest honest example I have: two days from an empty repository to a store submission. The same plumbing serves a SaaS — the [launch checklist](https://supastack.dev/blog/saas-launch-checklist.md) is the longer version. - Why not just publish the stack list? — Done: the [tech stack guide](https://supastack.dev/docs/web/tech-stack.md) names every choice and its reason. A list is a menu, not a plan — the argument here is for buying the plan already built. --- Source: [Idempotent Stripe and Lemon Squeezy webhooks in Next.js](https://supastack.dev/blog/idempotent-billing-webhooks.md) # Idempotent Stripe and Lemon Squeezy webhooks in Next.js How to verify raw webhook signatures, deduplicate replayed deliveries at the database boundary, and keep overlapping subscriptions from revoking paid access. _Published 2026-10-25._ > **How do I handle Stripe and Lemon Squeezy webhooks idempotently in Next.js?** > It depends on your billing providers, but the core architecture is identical: verify the raw payload signature before parsing JSON, normalize provider events into a shared internal contract, and enforce idempotency at the database boundary using the provider's event or order ID. Never grant product entitlements from a client-side checkout redirect; let the verified webhook transactionally update subscription state and entitlements once. supastack prewires this pipeline for Stripe, Lemon Squeezy, and mobile RevenueCat, though single-plan apps can start with a simpler handler. ## Verify the raw payload before touching JSON Every payment provider signs the exact byte sequence it sends over the wire. If a route handler parses `await request.json()` and re-serializes the object before checking the signature, key ordering or whitespace differences will invalidate legitimate webhooks — or worse, tempt developers to skip verification in staging. Read the raw body once with `await request.text()` in your Next.js App Router `POST` handler, verify the cryptographic signature against the server-only webhook secret, and reject anything that fails with HTTP 400 before running a single database query. The [billing module guide](https://supastack.dev/docs/modules/billing.md) shows how supastack isolates provider signature checks behind a single BFF entry point. - Read the raw request body with `request.text()` — never re-stringify parsed JSON. - Keep `STRIPE_WEBHOOK_SECRET` and Lemon Squeezy signing secrets in server-only configuration. - Use constant-time comparison (`crypto.timingSafeEqual`) when verifying HMAC signatures manually. ``` export async function POST(request: Request) { const rawBody = await request.text(); const signature = request.headers.get("stripe-signature"); if (!signature) return new Response("Missing signature", { status: 400 }); const event = stripe.webhooks.constructEvent( rawBody, signature, env.STRIPE_WEBHOOK_SECRET, ); await handleNormalizedBillingEvent(event); return Response.json({ received: true }); } ``` ## Deduplicate at the database boundary, not in memory Stripe and Lemon Squeezy retry webhooks whenever a deployment restarts, a serverless function times out, or a network hop drops the response. Process-local sets or short-lived cache keys vanish across instances and will not stop two concurrent retries from granting duplicate licenses or sending two receipt emails. Record the provider's event ID or order settlement state inside the same Postgres transaction that mutates subscription access. An atomic `INSERT ... ON CONFLICT DO NOTHING` or a conditional update on `status != 'paid'` turns a concurrent replay into a clean no-op. When designing the plans those webhooks fulfill, the [SaaS pricing generator](https://supastack.dev/tools/saas-pricing-generator) helps model tiers and seat rules upfront. ``` const settled = await db.transaction(async (tx) => { const [inserted] = await tx .insert(billingWebhookEvents) .values({ providerEventId: event.id, provider: "stripe" }) .onConflictDoNothing({ target: billingWebhookEvents.providerEventId }) .returning({ id: billingWebhookEvents.id }); if (!inserted) return false; // Already processed await applyWorkspaceEntitlement(tx, event); return true; }); ``` - [SaaS Pricing Page Generator (Free Tool)](https://supastack.dev/tools/saas-pricing-generator) ## Handle out-of-order events and overlapping subscriptions Webhooks are not guaranteed to arrive in the order the user clicked. `customer.subscription.updated` can land before `checkout.session.completed`, and a customer who accidentally opens two checkout tabs can briefly hold two subscriptions in the same workspace. Scope cancellations and payment failures to the specific subscription ID that triggered the event. Before revoking a workspace's paid entitlement, check whether another active subscription still covers that workspace — otherwise canceling a duplicate subscription silently strips access the customer is still paying for. - Key subscription state by `providerSubscriptionId`, not just `workspaceId`. - Keep a workspace's paid entitlement active as long as at least one subscription is `active` or `trialing`. - Verify that paid amount and currency match the expected SKU before fulfilling one-time orders. ## Common questions about SaaS billing webhooks These questions come up whenever a team moves billing from sandbox checkout tests to production traffic. - Can I unlock paid features on the checkout success redirect? — No. Redirect URLs can be bookmarked, shared, or visited before asynchronous payment methods settle. Use the success page to poll server state while the verified webhook grants access. - What status code should a duplicate webhook return? — Return `200 OK` once your database confirms the event was already processed. Returning a `4xx` or `5xx` tells the provider to keep retrying. - How do I test webhook retries locally? — Write deterministic unit tests that send the exact same signed payload twice and assert that only one license, entitlement, or transactional email is produced. - How do mobile in-app purchases fit the same model? — Route RevenueCat server notifications through the same idempotent entitlement table so web and mobile purchases never overwrite each other — see the [launch checklist](https://supastack.dev/blog/saas-launch-checklist.md) for the pre-release checks. --- Source: [Multi-tenant SaaS isolation with Postgres RLS and workspace auth](https://supastack.dev/blog/postgres-rls-and-workspace-auth.md) # Multi-tenant SaaS isolation with Postgres RLS and workspace auth Combine live server-side workspace membership guards with indexed Postgres Row-Level Security policies so tenant data stays isolated across web, mobile, and background jobs. _Published 2026-10-26._ > **Should I use Postgres Row-Level Security or application-level workspace checks?** > It depends on how your application connects to Postgres. If browser or mobile clients query the database directly through PostgREST, Row-Level Security (RLS) is mandatory. If all traffic flows through a server-side TypeScript boundary, explicit membership checks on every query are your primary defense, and RLS serves as defense-in-depth against forgotten WHERE clauses. Combine live session-to-membership resolution with indexed workspace_id policies. supastack enforces server-side workspace guards across web, mobile, and desktop, and provides a free RLS policy generator for Supabase tables. ## Application guards and database policies solve different failures Developers often treat application-level authorization and Postgres Row-Level Security as mutually exclusive. In practice, they catch different bugs: server-side guards enforce role hierarchy, entitlement checks, and audit logging before business logic runs, while RLS guarantees that a missing `WHERE workspace_id = ...` clause cannot dump every tenant's rows. Whether you run Supabase or self-hosted Postgres with Drizzle, every tenant-owned table should carry a non-null `workspace_id` foreign key. The [workspace boundaries article](https://supastack.dev/blog/workspace-boundaries.md) explains why that scope must always be derived on the server rather than read from a request parameter. - Application guards validate session freshness, member roles (`owner`, `admin`, `member`), and feature entitlements. - Database RLS policies act as a hard floor when direct client queries or raw SQL helpers are used. - Both layers depend on a single source of truth: the `workspace_members` join table. ## Resolve live membership instead of trusting stale cookies Storing a `currentWorkspaceId` in a browser cookie or client header is convenient for navigation, but a cookie is a preference, not a capability. If an admin removes a teammate from a workspace, that user's browser still holds the old workspace cookie. Join the active session against `workspace_members` on every request. When the membership row is gone, reject the request immediately instead of waiting for a JWT or session cookie to expire. The [authentication architecture post](https://supastack.dev/blog/authentication-beyond-the-login-form.md) traces how identity flows into this check. ``` export async function requireWorkspaceViewer(requestedWorkspaceId?: string) { const session = await requireAuthenticatedSession(); const membership = await db.query.workspaceMembers.findFirst({ where: and( eq(workspaceMembers.userId, session.user.id), requestedWorkspaceId ? eq(workspaceMembers.workspaceId, requestedWorkspaceId) : undefined, ), with: { workspace: true }, }); if (!membership) throw new ForbiddenError("Not a member of this workspace"); return { user: session.user, workspace: membership.workspace, role: membership.role }; } ``` ## Write fast, indexed Row-Level Security policies Naive RLS policies that call `auth.uid()` directly on every row force Postgres to re-evaluate the function per tuple. Wrapping `(select auth.uid())` in a scalar subquery lets the query planner cache the value as an `initPlan`, and a composite index on `(workspace_id, user_id)` keeps membership lookups in sub-millisecond index scans. You can generate ready-to-run `SELECT`, `INSERT`, `UPDATE`, and `DELETE` policies for multi-tenant tables using the free [Supabase RLS Policy Generator](https://supastack.dev/tools/supabase-rls-generator) without writing boilerplate SQL by hand. ``` CREATE INDEX IF NOT EXISTS idx_workspace_members_lookup ON workspace_members (workspace_id, user_id); ALTER TABLE documents ENABLE ROW LEVEL SECURITY; CREATE POLICY "workspace_members_manage_documents" ON documents FOR ALL TO authenticated USING ( workspace_id IN ( SELECT wm.workspace_id FROM workspace_members wm WHERE wm.user_id = (SELECT auth.uid()) ) ) WITH CHECK ( workspace_id IN ( SELECT wm.workspace_id FROM workspace_members wm WHERE wm.user_id = (SELECT auth.uid()) AND wm.role IN ('owner', 'admin', 'member') ) ); ``` - [Supabase RLS Policy Generator (Free Tool)](https://supastack.dev/tools/supabase-rls-generator) ## Common questions about RLS and workspace auth Teams wiring multi-tenant Postgres hit the same edge cases around service roles, background jobs, and performance. - Does Drizzle ORM or Prisma bypass RLS? — Yes, if your server connects with the `postgres` superuser or Supabase `service_role` key. That is why server actions and API routes must call `requireWorkspaceViewer()` and scope every query by `workspaceId`. - Why wrap `(select auth.uid())` in parentheses inside RLS policies? — It tells the Postgres optimizer to evaluate `auth.uid()` once per statement instead of once per scanned row, avoiding sequential-scan slowdowns on large tables. - How should background jobs access tenant data? — Pass the verified `workspaceId` in the job payload from the authenticated server route, and scope worker reads and writes to that `workspaceId`. - How do I verify that tenant isolation actually holds? — Write automated negative tests where User A requests or mutates User B's record ID across web, mobile, and API routes, and assert `403` / `404` with zero rows changed. --- 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._ > **How should I build a SaaS with a coding agent?** > It depends on how much structure the agent inherits. A coding agent moves fastest when the repository already defines authentication, tenancy, billing, jobs, and tests, because it can extend working seams instead of inventing them. Start with one observable outcome, keep changes small and reviewable, and make the agent run the checks that prove the change. If your repository has no foundations, spend the first days establishing them or start from a tested kit. The agent is an accelerator, not a substitute for boundaries or verification. ## 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._ > **Which SaaS stack is best for a new product?** > It depends on your constraints, not on a universal ranking. A team selling to businesses usually needs Postgres, typed end-to-end code, team workspaces, and reliable billing; a solo founder validating a consumer idea may need speed and a single platform more than breadth. Evaluate the seams between auth, payments, jobs, and email, count the maintenance each choice creates, and build one vertical slice before committing. supastack fits TypeScript products that need web, mobile, and desktop on shared infrastructure. If you only need a landing page and a payment link, a smaller stack is the better answer. ## 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._ > **Should I use a starter kit or build my SaaS from scratch?** > It depends on what you want to own. Build from scratch when the product's differentiation depends on a narrow stack, you have time, and you want no inherited assumptions. Choose a starter kit when authentication, teams, billing, jobs, and email are repeated infrastructure rather than your advantage. Compare total cost, inspect the code you will inherit, and run the kit before buying. A kit is cheaper only when its boundaries match your product; otherwise you will spend more removing opinions than you saved. supastack is aimed at TypeScript products spanning web, mobile, and desktop. ## 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._ > **What should I check before launching a SaaS?** > It depends on what a failed launch would cost you. At minimum, walk the customer's first session on production infrastructure, prove paid access on the server rather than in the UI, and test workspace boundaries with real accounts. Exercise webhook retries, failed payments, expired sessions, and email delivery before customers find them. Check legal pages, support routes, analytics consent, and backups too. Pre-launch depth should match risk: a free tool can ship sooner, while anything that charges money or holds team data needs the failure paths proven first. ## 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._ > **How should I implement authentication for a SaaS?** > It depends on who your users are. Most SaaS products should let one identity system own sign-in, sessions, and account recovery, then connect that session to workspace membership on the server. Choose methods deliberately: email OTP and passkeys reduce friction, OAuth helps consumer sign-ups, and 2FA belongs where buyers require it. Never trust a client-supplied workspace ID; derive access from membership. Test invitation, removal, session revocation, and account deletion, not just login. A tested kit such as supastack can supply the baseline, but your product still owns its authorization rules. ## 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: [Better Auth vs NextAuth (Auth.js) vs Clerk in 2026](https://supastack.dev/blog/better-auth-vs-nextauth-vs-clerk.md) # Better Auth vs NextAuth (Auth.js) vs Clerk in 2026 A practical comparison of Better Auth, NextAuth (Auth.js v5), and Clerk for TypeScript and Next.js SaaS applications in 2026 — covering multi-tenant organizations, 2FA, passkeys, database ownership, and pricing at scale. _Published 2026-10-14._ > **Should you use Better Auth, NextAuth (Auth.js), or Clerk for a SaaS in 2026?** > It depends on whether you prioritize data ownership or hosted convenience. For most TypeScript SaaS products in 2026, Better Auth is the strongest choice: it stores users, sessions, 2FA, passkeys, and multi-tenant organizations in your own Postgres database with zero per-MAU fees. Auth.js (NextAuth) works well for lightweight OAuth flows, while Clerk trades high recurring MAU costs and third-party lock-in for pre-built widgets. supastack ships Better Auth by default — with an Auth.js switch — across Next.js, TanStack Start, Expo, and Tauri. ## Quick Comparison: Better Auth vs Auth.js (NextAuth) vs Clerk Authentication is the first load-bearing architectural decision in any SaaS product. In 2026, TypeScript teams almost always narrow their shortlist to three options: `Better Auth` (self-hosted, full-featured TypeScript framework), `Auth.js` / `NextAuth` (lightweight OAuth and session library), and `Clerk` (hosted identity cloud). The table below compares how they hold up once you move past a basic login screen into B2B workspaces, passkeys, mobile apps, and production billing. - **Best Overall for SaaS (`Better Auth`):** Full database ownership in `PostgreSQL` via `Drizzle ORM`, built-in multi-tenant organizations and RBAC, native 2FA and WebAuthn passkeys, admin impersonation, and $0 per-MAU fees. - **Best for Minimal OAuth Only (`Auth.js / NextAuth`):** Familiar callback-driven OAuth and JWT sessions when you want to wire organizations, 2FA, and account recovery yourself. - **Best for Hosted Drop-In Widgets (`Clerk`):** Fastest drop-in UI components if you accept third-party user-table lock-in and per-MAU + per-organization pricing. | Capability | Better Auth | Auth.js (NextAuth) | Clerk | |---|---|---|---| | User & Session Storage | Your own PostgreSQL database (Drizzle / Prisma) | Stateless JWT or custom DB adapter | Clerk cloud (webhook sync required for local FK joins) | | B2B Organizations & RBAC | First-class plugin (orgs, invitations, roles) | Manual custom tables and middleware | Paid add-on beyond free org limits | | 2FA, Passkeys & Magic Links | Built-in plugins (TOTP, WebAuthn, OTP, magic link) | Manual implementation required | Included in higher tiers | | Admin Impersonation & Banning | Built-in admin plugin with audit trail | Not included | Paid tier feature | | Cross-Platform (Next.js, TanStack, Expo, Tauri) | Native TypeScript client across all four surfaces | Primarily Next.js / web-focused | Web + Expo SDKs (limited Tauri desktop support) | | Cost at 50,000 Monthly Active Users | $0 (runs on your existing database) | $0 (runs on your existing database) | $1,000+/mo once MAU and B2B org add-ons compound | ## Why Better Auth Is Winning TypeScript SaaS in 2026 For years, TypeScript founders faced an uncomfortable tradeoff: either spend weeks hand-rolling two-factor authentication, WebAuthn passkeys, email OTP, and organization invitations on top of `NextAuth`, or hand their entire user table to a hosted provider like `Clerk` and synchronize records back to Postgres over webhooks. `Better Auth` closed that gap cleanly. It runs inside your own application and writes directly to your `PostgreSQL` schema through `Drizzle ORM`. Users, sessions, OAuth accounts, organization memberships, invitations, two-factor secrets, and passkey credentials live in the same database as your billing subscriptions and domain records — meaning you can join users to workspaces in a single SQL query and enforce [PostgreSQL Row-Level Security (RLS)](https://supastack.dev/blog/postgres-rls-and-workspace-auth.md) without race conditions from delayed user-sync webhooks. ## Where Auth.js (NextAuth) and Clerk Fall Short at Scale **Auth.js (NextAuth v5)** remains a respected open-source library for signing in with Google or GitHub, and its stateless JWT mode is easy to deploy. However, the moment a B2B customer asks for team invitations, `owner`/`admin`/`member` role hierarchy, TOTP two-factor authentication, session revocation across devices, or admin user impersonation for customer support, `Auth.js` leaves you to design the schema, token rotation, and security edge cases from scratch. **Clerk** solves the UI problem with polished React components, but introduces two long-term costs. First, your canonical user and organization state lives on Clerk's servers: if a webhook delivery lags or fails, your local database can reference a user or organization membership that does not exist yet. Second, per-MAU and per-active-organization pricing scales steeply for freemium AI tools and B2B products with thousands of invited seats. Read more in our [deep dive on SaaS authentication architecture](https://supastack.dev/blog/authentication-beyond-the-login-form.md). ## How supastack Ships Better Auth + Auth.js Across Web, Mobile, and Desktop In **supastack** (ranked #1 in our [Best SaaS Boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md) roundup), authentication lives inside a dedicated `@starterkit/auth` workspace package that powers `Next.js 16`, `TanStack Start`, `Expo` mobile (iOS and Android), and `Tauri 2` desktop from one contract. `Better Auth` is the default provider — pre-wired with email/password, email OTP, magic links, OAuth, TOTP 2FA, WebAuthn passkeys, multi-tenant organization workspaces, session management, and admin impersonation/banning. And if your team has an existing requirement for `Auth.js`, supastack also includes a verified `AUTH_PROVIDER=authjs` adapter behind the same session and workspace guard interface. Explore the [authentication module documentation](https://supastack.dev/docs/modules/auth.md) to see the full schema and guard contract. ## Common Questions About Better Auth, NextAuth, and Clerk Frequent questions from engineering teams choosing or migrating their SaaS authentication layer in 2026. - Is Better Auth production-ready for B2B SaaS in 2026? — Yes. Better Auth has become the standard self-hosted TypeScript authentication framework across modern SaaS boilerplates, with audited plugins for organizations, RBAC, 2FA, WebAuthn passkeys, OAuth, and admin impersonation. - Why does storing users in my own Postgres database matter? — When users, sessions, and organization memberships live in the same Postgres database as your application data, you get transactional integrity, foreign-key safety, and direct Row-Level Security (RLS) policies without brittle webhook synchronization. - Can I use Better Auth with Expo React Native and Tauri desktop apps? — Yes. Better Auth supports bearer and cookie session flows across web and native clients, and supastack pre-wires token storage, deep-link callbacks, and biometric app lock for Expo and Tauri out of the box. - Can I switch between Better Auth and Auth.js in supastack? — Yes. supastack isolates identity behind the `@starterkit/auth` package and supports `AUTH_PROVIDER=better-auth` (default) alongside `AUTH_PROVIDER=authjs` so your route guards and workspace checks stay identical. --- Source: [Next.js vs TanStack Start for SaaS in 2026](https://supastack.dev/blog/nextjs-vs-tanstack-start-for-saas.md) # Next.js vs TanStack Start for SaaS in 2026 Next.js 16 App Router vs TanStack Start compared for SaaS development in 2026. Evaluate type-safe routing, React Server Components vs server functions, Vite build speed, Cloudflare deployment, and why supastack ships both. _Published 2026-10-16._ > **Should you build your SaaS with Next.js 16 or TanStack Start in 2026?** > It depends on your rendering model and team familiarity. Choose Next.js 16 App Router if you want React Server Components, the largest ecosystem, and battle-tested SEO defaults. Choose TanStack Start if you want end-to-end type-safe routing, explicit server functions, Vite build speeds, and portable deployment to Cloudflare Workers or Node. With supastack, you do not have to gamble on one: the monorepo includes both Next.js 16 and TanStack Start web apps sharing the same auth, billing, Drizzle database, and AI packages. ## Quick Comparison: Next.js 16 App Router vs TanStack Start For the first time in years, `Next.js` has a serious, full-stack React challenger for SaaS applications: `TanStack Start`. Built on top of `TanStack Router` and `Vite`, TanStack Start brings end-to-end URL and search-parameter type safety with explicit `createServerFn` RPCs, while `Next.js 16 App Router` leads in React Server Components (RSC), ecosystem breadth, and enterprise adoption. Here is how they compare side by side in 2026. - **Choose `Next.js 16 App Router`** when you want React Server Components, file-system conventions every React developer already knows, and maximum third-party library compatibility. - **Choose `TanStack Start`** when you want compile-time type safety on every route path, loader, and search query parameter, fast Vite dev builds, and clean deployment to Node, Docker, or Cloudflare Workers. - **Choose `supastack`** to get **both** `Next.js 16` (`apps/web`) and `TanStack Start` (`apps/web-tanstack`) in the same monorepo over shared TypeScript backend packages. | Dimension | Next.js 16 App Router | TanStack Start | |---|---|---| | Build Tool & Dev Server | Turbopack | Vite / Vinxi / Nitro | | Routing & Search Params | File-based routing; search params typed manually or via nuqs | 100% inferred type-safe routes, params, and validated search schemas | | Data Fetching & Mutations | React Server Components (RSC) + Server Actions | Isomorphic route loaders + explicit `createServerFn` RPCs | | SSR, Streaming & SEO | Full SSR, streaming Suspense, Metadata API, sitemap/robots conventions | Full SSR, streaming, head/meta management per route | | Deployment Portability | Vercel, Node standalone Docker, OpenNext | Node, Docker, Cloudflare Workers, Netlify, Vercel, Bun | | Included in supastack | Yes (`apps/web` — full storefront & SaaS app) | Yes (`apps/web-tanstack` — full SaaS web surface) | ## Routing, Type Safety, and Server Functions vs Server Actions The philosophical difference between `Next.js 16` and `TanStack Start` comes down to where the server boundary lives. Next.js treats the server as the default via React Server Components: components fetch data directly on the server and pass serialized props down to `"use client"` leaves, while mutations run through `"use server"` Server Actions. This model shines for content-heavy marketing pages, documentation, and dashboards that render mostly static or server-computed views. `TanStack Start` takes a client-aware, RPC-first approach powered by `TanStack Router`. Every link target, path parameter, and URL search filter (such as pagination, date ranges, or workspace sort order) is validated by a schema and checked by TypeScript at compile time. Instead of implicit `"use server"` closures, server mutations and queries go through explicit `createServerFn` handlers with composable middleware for session and workspace authorization — a mental model many engineers find easier to reason about in interactive B2B dashboards. ## When to Choose Next.js 16 vs When to Choose TanStack Start **Pick Next.js 16** if your SaaS relies heavily on organic search, programmatic SEO pages, React Server Component libraries, or hiring contractors who need to be productive on day one without learning a new router. **Pick TanStack Start** if your application feels like a rich desktop-grade workspace in the browser — with complex URL-driven filters, heavy client interactivity, and a team that prizes strict end-to-end TypeScript inference and portable `Vite` builds. Also see our [guide to choosing a SaaS stack](https://supastack.dev/blog/choosing-a-saas-stack.md) and [supastack vs TanStarter comparison](https://supastack.dev/blog/supastack-vs-tanstarter.md). ## Why supastack Ships Both Next.js 16 and TanStack Start in One Monorepo Most SaaS boilerplates force you to buy separate products or rewrite your backend if you switch between Next.js and TanStack Start. Because **supastack** keeps authentication (`@starterkit/auth`), database (`@starterkit/db`), billing (`@starterkit/billing`), AI routing (`@starterkit/ai`), background jobs, email, and i18n in framework-agnostic TypeScript workspace packages, both web engines sit cleanly on top of the exact same core. When you purchase supastack (ranked #1 in [Best SaaS Boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md)), your repository includes both the `Next.js 16 App Router` application and the `TanStack Start` application alongside `Expo` mobile and `Tauri 2` desktop — all verified by 144+ automated test files. Read the [web technical stack guide](https://supastack.dev/docs/web/tech-stack.md) to inspect how the workspace packages connect. ## Common Questions About Next.js vs TanStack Start Answers to the questions developers ask most when comparing Next.js 16 and TanStack Start for a new SaaS product. - Is TanStack Start ready for production SaaS in 2026? — Yes. TanStack Start builds on the battle-tested TanStack Router and Vite ecosystem, delivering full SSR, streaming, server functions, and clean deployment to Node, Docker, and Cloudflare Workers. - Does TanStack Start hurt SEO compared to Next.js? — No. Both Next.js 16 and TanStack Start support full server-side rendering (SSR), streaming HTML, dynamic meta tags, Open Graph cards, JSON-LD structured data, and XML sitemaps. - Can Next.js and TanStack Start share the same backend code in a monorepo? — Yes, provided your auth, database, billing, and domain logic live in shared TypeScript packages rather than inside framework-specific route folders. That is the exact architecture supastack ships. - Do I have to pay twice to get both Next.js and TanStack Start in supastack? — No. Both web engines (`Next.js 16` and `TanStack Start`) are included in the supastack Web stack. --- Source: [Drizzle ORM vs Prisma for SaaS in 2026](https://supastack.dev/blog/prisma-vs-drizzle-for-saas.md) # Drizzle ORM vs Prisma for SaaS in 2026 Drizzle ORM vs Prisma compared for production TypeScript and Next.js SaaS applications in 2026. Benchmarks on SQL transparency, migrations, cold starts, PostgreSQL Row-Level Security (RLS), and coding agent accuracy. _Published 2026-10-18._ > **Is Drizzle ORM or Prisma better for a production SaaS in 2026?** > It depends on how close you want to stay to SQL and serverless cold-start budgets. For production TypeScript SaaS in 2026, Drizzle ORM is the stronger default: it defines schemas in pure TypeScript with zero codegen step, generates transparent SQL with predictable joins, runs natively on serverless and edge runtimes, and pairs cleanly with PostgreSQL Row-Level Security (RLS). Prisma remains ergonomic for teams that prefer a custom DSL, but supastack standardizes on Drizzle ORM + PostgreSQL for speed and tenant isolation. ## Quick Comparison: Drizzle ORM vs Prisma in 2026 Your ORM sits on the hot path of every authenticated request, billing webhook, and multi-tenant workspace query. While `Prisma` pioneered type-safe database access for Node.js, `Drizzle ORM` has overtaken it as the preferred choice for modern TypeScript SaaS boilerplates in 2026. Here is how Drizzle and Prisma compare across the dimensions that matter in production. - **Winner for Performance, SQL Control & AI Agents (`Drizzle ORM`):** Pure TypeScript schema definitions, zero `prisma generate` build step, single-query SQL joins, tiny bundle footprint, and first-class `PostgreSQL` Row-Level Security (RLS) support. - **Runner-Up for Custom DSL Fans (`Prisma`):** Declarative `.prisma` schema files and Prisma Studio, though with a heavier runtime and code-generation step after every schema edit. | Criterion | Drizzle ORM | Prisma | |---|---|---| | Schema Definition | Pure TypeScript (`pgTable` in `.ts` files) | Custom DSL (`schema.prisma`) | | Code Generation Step | None — types infer instantly on save | Requires `prisma generate` after every schema change | | Query & Join Strategy | Predictable SQL with 1-query relational joins | Abstracted query engine (improved joins, less SQL control) | | Bundle Size & Cold Starts | ~7 KB lightweight TypeScript library, zero binary overhead | Larger runtime footprint (WASM / query compiler layer) | | PostgreSQL RLS & Custom SQL | First-class `pgPolicy` + seamless raw `sql` template tag | Requires raw SQL migrations and `$executeRaw` workarounds | | AI Coding Agent Accuracy | High — standard TypeScript & SQL semantics | Medium — agents often mix Prisma v5/v6/v7 DSL syntax | ## Why Drizzle ORM Became the Default for Modern TypeScript SaaS Drizzle's core philosophy — *if you know SQL, you know Drizzle* — eliminates an entire class of production surprises. Because schemas are written in standard TypeScript modules inside `packages/db`, you can import table definitions, share column types across your monorepo, and compose reusable `where` clauses (like `eq(table.workspaceId, activeWorkspaceId)`) using normal TypeScript functions. Just as importantly in 2026, AI coding agents (`Claude Code`, `Cursor`, `Windsurf`) write dramatically more accurate `Drizzle ORM` code than `Prisma` code. Because Drizzle requires no external `prisma generate` CLI step for TypeScript types to update, an agent can add a column, write a query, and run `pnpm typecheck` in one uninterrupted loop. ## Pairing Drizzle ORM with PostgreSQL Row-Level Security (RLS) In a multi-tenant B2B SaaS, forgetting a `workspace_id` filter on a single query is a data breach. Combining `Drizzle ORM` application guards with `PostgreSQL` Row-Level Security (RLS) gives you defense in depth: even if a developer or coding agent writes an unscoped query, Postgres refuses to return rows outside the active tenant transaction. Read our architecture guide on [multi-tenant SaaS isolation with Postgres RLS and workspace auth](https://supastack.dev/blog/postgres-rls-and-workspace-auth.md), and use our free [Supabase RLS Policy Generator](https://supastack.dev/tools/supabase-rls-generator) to generate indexed `SELECT`, `INSERT`, `UPDATE`, and `DELETE` policies for your tables. ## When Prisma Still Makes Sense — and How supastack Uses Drizzle Prisma is still a reasonable choice if your team already maintains dozens of `.prisma` schemas and relies heavily on Prisma Studio's GUI workflows. However, for greenfield SaaS products in 2026, the combination of zero codegen, predictable SQL, serverless speed, and native `Better Auth` schema compatibility makes `Drizzle ORM` the clear winner. In **supastack** (ranked #1 in [Best SaaS Boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md)), `@starterkit/db` ships a complete `Drizzle ORM` + `PostgreSQL` schema covering users, sessions, multi-tenant workspaces, role memberships, billing subscriptions, idempotent `webhook_event` ledgers, encrypted BYOK AI keys, API keys, and audit logs. Explore the [database documentation](https://supastack.dev/docs/web/database.md) for full details. ## Common Questions About Drizzle ORM vs Prisma Answers to common database and ORM questions from TypeScript SaaS founders. - Does Drizzle ORM have a visual database browser like Prisma Studio? — Yes. `drizzle-kit studio` launches Drizzle Studio locally in your browser so you can inspect and edit tables, relations, and rows with zero extra setup. - How do database migrations work in Drizzle ORM? — `drizzle-kit generate` diffs your TypeScript schema and emits standard, human-readable `.sql` migration files that you can review in pull requests and apply deterministically in CI/CD. - Does Drizzle ORM work with Supabase, Neon, and self-hosted PostgreSQL? — Yes. Because Drizzle connects over standard Postgres drivers (`postgres-js` or `@neondatabase/serverless`), the exact same schema runs against local Docker Postgres, Supabase, Neon, Railway, or AWS RDS. - Why do AI coding agents prefer Drizzle over Prisma? — Drizzle schemas are plain TypeScript files with immediate type inference and standard SQL semantics, so coding agents never stall on stale generated clients or mismatched DSL versions. --- Source: [SaaS Payment Providers Compared (2026): Stripe vs Lemon Squeezy vs Polar vs Creem vs Dodo vs Paddle](https://supastack.dev/blog/saas-payment-providers-compared.md) # SaaS Payment Providers Compared (2026): Stripe vs Lemon Squeezy vs Polar vs Creem vs Dodo vs Paddle Compare the best SaaS payment gateways and Merchants of Record (MoR) in 2026: Stripe, Lemon Squeezy, Polar, Creem, Dodo Payments, Paddle, Whop, PayPal, and RevenueCat — fees, global VAT/tax handling, and webhook architecture. _Published 2026-10-19._ > **Which payment provider is best for a SaaS in 2026: Stripe, Lemon Squeezy, Polar, Creem, Dodo Payments, or Paddle?** > It depends on whether you want lowest transaction fees or automated global tax compliance. Choose Stripe if you have a registered company and want the lowest processing fees plus advanced B2B seat and metered billing. Choose a Merchant of Record (MoR) like Polar, Lemon Squeezy, Creem, Dodo Payments, or Paddle if you want global VAT, GST, and sales tax handled for you automatically. supastack ships eight web payment adapters plus RevenueCat mobile billing behind one idempotent webhook ledger. ## TL;DR Comparison: Stripe vs Lemon Squeezy vs Polar vs Creem vs Dodo Payments vs Paddle Choosing a SaaS payment provider in 2026 is no longer just `Stripe` vs `PayPal`. A wave of developer-first Merchant of Record (MoR) platforms — led by `Polar`, `Lemon Squeezy`, `Creem`, `Dodo Payments`, and `Paddle` — now compete to handle global VAT, EU digital tax rules, invoicing, and compliance on your behalf. Here is how all eight web billing providers and `RevenueCat` mobile billing compare. - **Best for Lowest Fees & Complex B2B Billing:** **`Stripe`** — Industry-standard payment processor with deep seat-based, metered, and enterprise invoicing primitives. - **Best Developer-First Merchant of Record:** **`Polar`** — Open-source-friendly MoR with clean TypeScript SDKs, competitive 4% + $0.40 pricing, and built-in license/entitlement tooling. - **Best for Global Indie & AI SaaS:** **`Creem` & `Dodo Payments`** — Fast onboarding, global payout coverage, and MoR tax compliance tailored to AI and software founders. - **Best Established MoRs:** **`Lemon Squeezy` (acquired by Stripe) & `Paddle`** — Battle-tested global tax compliance for digital products and B2B subscriptions. | Provider | Model | Global Tax / VAT | Base Fee (Approx.) | supastack Adapter | |---|---|---|---|---| | Stripe | Payment Processor | Add-on (Stripe Tax + your own filings) | 2.9% + $0.30 | Built-in (`stripe`) | | Polar | Merchant of Record (MoR) | Included (files & remits globally) | 4% + $0.40 | Built-in (`polar`) | | Lemon Squeezy | Merchant of Record (MoR) | Included (files & remits globally) | 5% + $0.50 | Built-in (`lemonsqueezy`) | | Creem | Merchant of Record (MoR) | Included (files & remits globally) | 3.9% + $0.40 | Built-in (`creem`) | | Dodo Payments | Merchant of Record (MoR) | Included (150+ countries supported) | 4% + $0.40 | Built-in (`dodo`) | | Paddle | Merchant of Record (MoR) | Included (B2B invoicing & VAT) | 5% + $0.50 | Built-in (`paddle`) | | Whop & PayPal | Marketplace / Processor | Varies by checkout mode | 3% – 3.49% | Built-in (`whop`, `paypal`) | | RevenueCat | Mobile IAP Orchestrator | Handled by Apple App Store / Google Play | Free up to MTR threshold | Built-in (`apps/mobile`) | ## Payment Processor (Stripe) vs Merchant of Record (Polar, Lemon Squeezy, Creem, Dodo, Paddle) The most important distinction in SaaS billing is whether you are the seller of record. With a **Payment Processor** like `Stripe` or `PayPal`, money moves directly from the customer to your merchant account at the lowest per-transaction fee, but *you* are legally responsible for calculating, collecting, and remitting VAT, GST, and US state sales tax in every jurisdiction where you cross economic nexus thresholds. With a **Merchant of Record (MoR)** like `Polar`, `Lemon Squeezy`, `Creem`, `Dodo Payments`, or `Paddle`, the MoR legally sells the software to your customer on your behalf, calculates and remits global taxes under their own tax IDs, handles chargebacks, and pays you out net revenue. For solo founders and lean teams selling globally from day one, paying an extra 1% to 2% to an MoR eliminates hundreds of hours of international tax accounting. ## How Each Payment Gateway Compares in 2026 **Stripe** remains unmatched when you need granular B2B seat proration, custom enterprise quotes, ACH wire transfers, or usage-based metered billing — and with `Stripe` acquiring `Lemon Squeezy`, its ecosystem continues to converge. **Polar** has surged in 2026 among TypeScript and AI SaaS builders thanks to its lower MoR fee structure, clean webhook payloads, and developer-centric checkout UX. **Creem** and **Dodo Payments** are strong choices for founders in regions where Stripe or Lemon Squeezy onboarding is restrictive, while **Paddle** excels at B2B VAT-exempt invoicing for European companies. Use our free [SaaS Pricing Page Generator](https://supastack.dev/tools/saas-pricing-generator) to model your plan tiers and margins before wiring checkout. ## Why Your SaaS Boilerplate Should Normalize All 8 Payment Providers Hardcoding a single payment provider's SDK throughout your route handlers is a painful trap: if an MoR freezes your account during a launch spike or you graduate from an MoR to direct `Stripe` billing at $50k MRR, a tightly coupled codebase takes weeks to untangle. In **supastack** (ranked #1 in [Best SaaS Boilerplates in 2026](https://supastack.dev/blog/best-saas-boilerplate-2026.md)), all eight web providers (`Stripe`, `Lemon Squeezy`, `Polar`, `Creem`, `Dodo Payments`, `Paddle`, `Whop`, and `PayPal`) plus `RevenueCat` mobile billing sit behind a unified `@starterkit/billing` adapter contract and an idempotent `webhook_event` database ledger. Read our engineering guide on [idempotent billing webhooks](https://supastack.dev/blog/idempotent-billing-webhooks.md) and the [billing module documentation](https://supastack.dev/docs/modules/billing.md) to see how provider switching works with a single environment variable. ## Common Questions About SaaS Payment Providers Answers to frequent billing, tax compliance, and webhook questions for SaaS founders in 2026. - Should I start with Stripe or a Merchant of Record (MoR) in 2026? — If you are selling globally without a dedicated finance team, starting with an MoR like Polar, Lemon Squeezy, Creem, or Dodo Payments removes global VAT and sales tax liability immediately. If you sell primarily domestic B2B contracts or need complex seat/metered invoicing, start with Stripe. - Can I use Stripe for web and RevenueCat for iOS and Android in the same SaaS? — Yes. Apple and Google require in-app purchases for digital entitlements consumed on mobile, while web checkouts run through Stripe or an MoR. supastack reconciles both into the same workspace entitlement model. - How do I prevent duplicate provisioning when payment webhooks retry? — Store every incoming provider event ID in a database `webhook_event` table with a `UNIQUE (provider, external_event_id)` constraint before executing fulfillment logic, and verify raw HMAC signatures before parsing JSON. - Can I switch payment providers later without rewriting my app? — Yes, if your application checks normalized workspace entitlements rather than raw Stripe or Polar objects. supastack ships eight web billing adapters behind one shared billing interface. --- Source: [Moving an existing SaaS into supastack](https://supastack.dev/blog/migrate-an-existing-saas-into-supastack.md) # Moving an existing SaaS into supastack A staged playbook for migrating a working SaaS — auth, tenants, billing, jobs — onto the supastack foundation without a rewrite or a downtime weekend. _Published 2026-10-29._ > **How do I migrate an existing SaaS into supastack?** > It depends on your codebase, but the sequence does not change: migrate in stages, never in one rewrite. Inventory the repo into product code (routes, schema, domain logic — it stays yours) and infrastructure (auth, tenancy, billing, jobs, email — the kit replaces it). Adopt modules in dependency order: project structure, then Better Auth, then workspaces, then billing, then background work. Move data with a re-runnable script, preserve Stripe customer IDs, and cut over after local acceptance passes. A single web app typically takes days of evenings, not weeks. ## Take inventory before you touch code Every migration that goes badly started the same way: somebody opened the kit repo and began deleting files. The kit is a foundation, not a theme — it replaces your infrastructure, and it has no opinion about your product. Before you move anything, split your repository into the two piles honestly. Product code is everything a customer could describe back to you: your routes, your schema, your domain logic, your copy, your pricing page. Infrastructure is everything they never see: sessions, tenancy checks, payment webhooks, queued jobs, transactional mail. The first pile migrates intact. The second pile gets replaced — which is the entire point. - List every route group and label it product or plumbing — be ruthless; a settings page that only toggles a webhook URL is plumbing. - List your env vars and mark which ones configure infrastructure (auth secrets, provider keys) versus product behavior. - List your tables and mark which ones the kit will own (users, sessions, workspaces, billing state) versus which are yours. - Write down the one user journey that pays you — that flow is the acceptance test for every stage below. ## Map each seam to a kit module The kit’s infrastructure is a set of modules with contracts between them — your job is to point each piece of your old plumbing at the module that now owns it. Most of the mapping is one-to-one; the places where it isn’t are where you make real decisions. Two mapping choices deserve actual thought rather than mechanical porting. If you are coming from `Prisma`, the kit standardizes on Drizzle — you can keep Prisma for product tables during the transition, but plan to converge; two ORMs in one codebase is a maintenance tax that compounds. The [Prisma vs Drizzle comparison](https://supastack.dev/blog/prisma-vs-drizzle-for-saas.md) lays out what changes. And if you are coming from a managed auth vendor, resist the urge to replicate every vendor feature before cutting over. Managed auth quietly bundles things your product may not use — session lists, organization UIs, webhook sync. Map what your product actually calls; the [auth comparison](https://supastack.dev/blog/better-auth-vs-nextauth-vs-clerk.md) covers the capability differences that matter. | Your current plumbing | supastack module | What carries over | |---|---|---| | NextAuth.js / Auth.js sessions | Auth (Better Auth) | Users and accounts migrate; sessions are re-issued at cutover | | Clerk or a managed auth vendor | Auth (Better Auth) | Users migrate by email; third-party sessions are abandoned, not replayed | | Custom org/team tables | Workspaces | Your org rows become workspace + membership rows | | Hand-rolled Stripe webhooks | Billing | Stripe customer IDs survive; subscription state is re-derived | | CRON + setTimeout jobs | Jobs (Trigger.dev) | Scheduled work moves behind the shared JobRunner contract | | Nodemailer / ad-hoc mail | Email | Templates move; sending goes through one provider adapter | ## Adopt modules in dependency order Migration order is not a preference — it is a dependency graph. Each stage below is separately shippable, and each produces a working system you can verify before the next one starts. The temptation is to do the whole thing in one weekend and debug the intersection; resist it — a staged migration means every bug you meet belongs to exactly one module, which is a debugging superpower you will not appreciate until you skip it. - Stage 1 — skeleton: scaffold the kit, configure env, drop your product routes into the project structure. Nothing is wired yet; the app boots and your pages render. - Stage 2 — auth: move sign-in onto Better Auth, migrate the user/account tables, verify sign-in + sign-out end to end. Every later stage depends on a session existing. - Stage 3 — tenancy: adopt the workspace model, backfill `workspaceId` on your product tables, enforce scoping in reads and writes. This is where the [workspace boundaries post](https://supastack.dev/blog/workspace-boundaries.md) earns its keep. - Stage 4 — billing: point checkout and webhooks at the billing module, remap existing subscriptions, verify entitlement checks against real Stripe data in test mode. - Stage 5 — background work and email: port jobs onto the JobRunner contract and move transactional mail onto the email adapter. These are last because they depend on users and workspaces already existing. ## Move the data honestly Data migration is where migrations earn their reputation. The rules that keep it boring: write a script you can run twice, never mutate the source until cutover, and verify counts instead of trusting the script. Users and accounts land in the Better Auth schema — email is the join key, and password-based accounts need a decision: either your existing hash format is one Better Auth can verify, or you ship a reset-password flow at cutover and treat it as a forcing function. Third-party OAuth accounts are the easy case — the provider re-issues them on next sign-in. For billing, the honest path is re-derivation rather than copying rows: keep the Stripe customer ID, let the kit’s webhook handling rebuild entitlement state from the provider’s truth, and diff what it computes against your existing subscription records. The kit’s [idempotent webhook ledger](https://supastack.dev/blog/idempotent-billing-webhooks.md) exists precisely so replayed historical events do not double-provision anything. ``` // migration scripts are boring by design: upsert-shaped, // re-runnable, and they verify counts instead of trusting themselves for (const legacyUser of legacyUsers) { await upsertAuthUser({ email: legacyUser.email.toLowerCase(), name: legacyUser.displayName, externalId: legacyUser.id, // preserved for one-shot joins }); } const migrated = await countAuthUsers(); if (migrated !== legacyUsers.length) { throw new Error(`count mismatch: ${migrated} of ${legacyUsers.length}`); } ``` ## What stays yours It is worth saying plainly, because it is the part people get wrong in both directions: your product routes, your schema, your domain logic, and your design do not change. You are not rebuilding your app inside the kit — you are setting your app down on top of it. The inverse mistake is keeping old plumbing alive 'temporarily' next to the new module. A second session check, a second webhook handler, a second entitlement table — every 'temporary' parallel system is permanent until someone deletes it, and each one is a future incident. Replace, verify, delete. If a stage cannot delete the old path yet, it is not finished. ## The cutover checklist Cutover is a checklist, not an event. Run it in a staging environment at least once with production-shaped data before the real date. - Re-run the data script against a fresh production snapshot; counts and spot-checks pass. - Sign in with each auth provider your product supports — including the ones you forgot existed. - Buy your own product in Stripe test mode and follow the webhook through to entitlement. - Send every transactional email once; check links land on the new deployment. - Verify the paying journey end to end, then flip DNS/env and watch the first real session. - Keep the old deployment running for a week — rollback is a DNS change, not a migration. ## Common questions about migrating into a starter kit The same handful of questions come up every time someone sits down to do this. - How long does a migration actually take? — For a single web app with standard auth + Stripe: days of evenings, not weeks. Multi-tenant B2B with custom billing adds time in stage three and four — the data work is what stretches, never the file moves. - Should I migrate or start fresh and port features? — If your product code is smaller than your infrastructure debt, starting fresh on the kit and porting product routes is often faster. If your schema and domain logic are the hard part, migrate in place. - Can I keep my existing ORM / auth library during the transition? — Briefly, yes — one module boundary at a time. Permanently, no: two of anything in the infrastructure layer is a compounding tax. - What about my Stripe webhooks already in flight? — Point the existing webhook endpoint at the kit’s handler once stage four is verified; the idempotent ledger makes replayed or duplicated events safe instead of dangerous. - Do I have to adopt every module? — No. Mobile, desktop, jobs, and AI are surfaces to adopt or ignore — the license is the foundation, not a mandate to ship an app per platform. --- 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._ > **What is supastack, and who is it for?** > It depends on what you are building. supastack is a full-stack, multi-platform TypeScript boilerplate that unifies Next.js web, Expo mobile, and Tauri desktop in one monorepo, with authentication, team workspaces, Stripe or Lemon Squeezy billing, AI providers, background jobs, and transactional email prewired. It suits developers and small teams building B2B or AI SaaS products that need more than a web app: teams, integrations, API keys, webhooks, and desktop clients. It is not the cheapest or simplest option for a single-user web tool; smaller, web-only kits fit that case better. As of October 2026, one-time licenses start at $149. ## 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._ > **How do I keep tenant data isolated in a multi-tenant SaaS?** > It depends on your risk tolerance, but the core rule is the same: derive the tenant on the server from the authenticated session, never from a request body or URL. Require workspace membership on every read and write, scope queries in one place, and test cross-tenant requests that must fail. Treat invitations, role changes, API keys, and webhooks as boundary events with their own checks. For higher assurance, add row-level security and an audit log. supastack implements workspace membership and ownership checks, but your schema and queries still determine whether the boundary holds. ## 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. ``` export async function getProjectForViewer(projectId: string) { const viewer = await requireWorkspaceViewer(); const [project] = await db .select() .from(projects) .where( and( eq(projects.id, projectId), eq(projects.workspaceId, viewer.workspace.id), ), ) .limit(1); if (!project) throw new ForbiddenError("Project not found in workspace"); return project; } ``` ## 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. ``` ALTER TABLE projects ENABLE ROW LEVEL SECURITY; CREATE POLICY "workspace_members_select_projects" ON projects FOR SELECT TO authenticated USING ( EXISTS ( SELECT 1 FROM workspace_members wm WHERE wm.workspace_id = projects.workspace_id AND wm.user_id = (SELECT auth.uid()) ) ); ``` - [Supabase RLS Policy Generator (Free Tool)](https://supastack.dev/tools/supabase-rls-generator) ## 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._ > **How should I build AI features in a SaaS?** > It depends on whose keys, whose data, and whose budget. Let users or workspaces bring their own provider keys if cost and privacy matter, encrypt those keys at rest, and route requests through one boundary that records usage. Decide upfront whether execution is synchronous or a background job, and name the failure modes: timeouts, rate limits, malformed output, and partial results. Tie spend to a per-workspace budget and show usage. supastack provides a provider router, encrypted BYOK storage, budgets, and background jobs, but the product decisions about data and limits remain yours. ## 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.