diff --git a/.github/workflows/deploy_all_examples.yml b/.github/workflows/deploy_all_examples.yml
index 52238ef560..3ae46c7b7d 100644
--- a/.github/workflows/deploy_all_examples.yml
+++ b/.github/workflows/deploy_all_examples.yml
@@ -35,6 +35,8 @@ jobs:
path: examples/react
- name: burn
path: examples/burn
+ - name: tanstack-db-web-starter
+ path: examples/tanstack-db-web-starter
env:
DEPLOY_ENV: 'production'
@@ -53,7 +55,13 @@ jobs:
ELECTRIC_API: ${{ secrets.ELECTRIC_API }}
ELECTRIC_ADMIN_API: ${{ secrets.ELECTRIC_ADMIN_API }}
ELECTRIC_TEAM_ID: ${{ secrets.ELECTRIC_TEAM_ID }}
+ ELECTRIC_ADMIN_API_AUTH_TOKEN: ${{ secrets.ELECTRIC_ADMIN_API_AUTH_TOKEN }}
ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_KEY }}
+ QUICKSTART_DATABASE_URI: ${{ secrets.QUICKSTART_DATABASE_URI }}
+ QUICKSTART_POOLED_DATABASE_URI: ${{ secrets.QUICKSTART_POOLED_DATABASE_URI }}
+ QUICKSTART_SOURCE_ID: ${{ vars.QUICKSTART_SOURCE_ID }}
+ QUICKSTART_SOURCE_SECRET: ${{ secrets.QUICKSTART_SOURCE_SECRET }}
+ BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
# HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} TODO
steps:
diff --git a/.github/workflows/deploy_examples.yml b/.github/workflows/deploy_examples.yml
index 6f820a7844..5125c013d2 100644
--- a/.github/workflows/deploy_examples.yml
+++ b/.github/workflows/deploy_examples.yml
@@ -38,6 +38,7 @@ jobs:
remix/**
react/**
burn/**
+ tanstack-db-web-starter/**
- name: Create example matrix
id: create-example-matrix
@@ -85,6 +86,7 @@ jobs:
remix: ${{ steps.deploy.outputs.remix }}
react: ${{ steps.deploy.outputs.react }}
burn: ${{ steps.deploy.outputs.burn }}
+ tanstack-db-web-starter: ${{ steps.deploy.outputs.tanstack-db-web-starter }}
env:
DEPLOY_ENV: ${{ github.event_name == 'push' && 'production' || format('pr-{0}', github.event.number) }}
@@ -108,6 +110,11 @@ jobs:
ELECTRIC_ADMIN_API_AUTH_TOKEN: ${{ secrets.ELECTRIC_ADMIN_API_AUTH_TOKEN }}
SECRET_KEY_BASE: ${{ secrets.LIVEVIEW_EXAMPLE_SECRET_KEY_BASE }}
ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_KEY }}
+ QUICKSTART_DATABASE_URI: ${{ secrets.QUICKSTART_DATABASE_URI }}
+ QUICKSTART_POOLED_DATABASE_URI: ${{ secrets.QUICKSTART_POOLED_DATABASE_URI }}
+ QUICKSTART_SOURCE_ID: ${{ vars.QUICKSTART_SOURCE_ID }}
+ QUICKSTART_SOURCE_SECRET: ${{ secrets.QUICKSTART_SOURCE_SECRET }}
+ BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
steps:
- uses: actions/checkout@v4
@@ -295,7 +302,8 @@ jobs:
"phoenix-liveview": "${{ needs.deploy.outputs.phoenix-liveview }}",
"tanstack": "${{ needs.deploy.outputs.tanstack }}",
"remix": "${{ needs.deploy.outputs.remix }}",
- "react": "${{ needs.deploy.outputs.react }}"
+ "react": "${{ needs.deploy.outputs.react }}",
+ "tanstack-db-web-starter": "${{ needs.deploy.outputs.tanstack-db-web-starter }}"
}
// Create deployments array only for examples that were deployed
diff --git a/.github/workflows/teardown_examples_pr_stack.yml b/.github/workflows/teardown_examples_pr_stack.yml
index ee33e4f4ab..5911b3d94e 100644
--- a/.github/workflows/teardown_examples_pr_stack.yml
+++ b/.github/workflows/teardown_examples_pr_stack.yml
@@ -29,6 +29,7 @@ jobs:
'remix',
'react',
'burn',
+ 'tanstack-db-web-starter',
]
env:
@@ -41,6 +42,9 @@ jobs:
NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }}
ELECTRIC_API: ${{ secrets.ELECTRIC_API }}
ELECTRIC_ADMIN_API: ${{ secrets.ELECTRIC_ADMIN_API }}
+ ELECTRIC_TEAM_ID: ${{ secrets.ELECTRIC_TEAM_ID }}
+ ELECTRIC_ADMIN_API_AUTH_TOKEN: ${{ secrets.ELECTRIC_ADMIN_API_AUTH_TOKEN }}
+ BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
# HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} TODO
steps:
diff --git a/examples/tanstack-db-web-starter/sst.config.ts b/examples/tanstack-db-web-starter/sst.config.ts
new file mode 100644
index 0000000000..15f9aaecbd
--- /dev/null
+++ b/examples/tanstack-db-web-starter/sst.config.ts
@@ -0,0 +1,82 @@
+///
+
+import { execSync } from "node:child_process"
+
+export default $config({
+ app(input) {
+ return {
+ name: `quickstart-example`,
+ removal: input?.stage === `production` ? `retain` : `remove`,
+ protect: [`production`].includes(input?.stage),
+ home: `aws`,
+ providers: {
+ cloudflare: `5.42.0`,
+ aws: {
+ version: `6.66.2`,
+ profile: process.env.CI ? undefined : `marketing`,
+ },
+ },
+ }
+ },
+ async run() {
+ // Validate required environment variables
+ if (!process.env.ELECTRIC_API) {
+ throw new Error(`ELECTRIC_API environment variable is required`)
+ }
+ if (!process.env.BETTER_AUTH_SECRET) {
+ throw new Error(`BETTER_AUTH_SECRET environment variable is required`)
+ }
+ if (
+ !process.env.QUICKSTART_DATABASE_URI ||
+ !process.env.QUICKSTART_POOLED_DATABASE_URI ||
+ !process.env.QUICKSTART_SOURCE_ID ||
+ !process.env.QUICKSTART_SOURCE_SECRET
+ ) {
+ throw new Error(
+ `QUICKSTART_DATABASE_URI, QUICKSTART_POOLED_DATABASE_URI, ` +
+ `QUICKSTART_SOURCE_ID, and QUICKSTART_SOURCE_SECRET are required`
+ )
+ }
+
+ // Apply migrations (idempotent)
+ applyDrizzleMigrations(process.env.QUICKSTART_DATABASE_URI)
+
+ const website = new sst.aws.TanStackStart(`quickstart-website`, {
+ environment: {
+ // Database
+ DATABASE_URL: process.env.QUICKSTART_POOLED_DATABASE_URI,
+
+ // Electric
+ ELECTRIC_URL: process.env.ELECTRIC_API,
+ ELECTRIC_SOURCE_ID: process.env.QUICKSTART_SOURCE_ID,
+ ELECTRIC_SOURCE_SECRET: process.env.QUICKSTART_SOURCE_SECRET,
+
+ // Better Auth
+ BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
+ BETTER_AUTH_URL: `https://quickstart.examples.electric-sql.com`,
+ },
+ domain: {
+ name: `quickstart.examples.electric-sql.com`,
+ dns: sst.cloudflare.dns(),
+ },
+ })
+
+ return {
+ website: website.url,
+ }
+ },
+})
+
+/**
+ * Apply migrations using Drizzle Kit.
+ * Migrations are in src/db/out/ (generated by drizzle-kit generate).
+ */
+function applyDrizzleMigrations(dbUri: string) {
+ console.log(`[quickstart] Applying Drizzle migrations`)
+ execSync(`pnpm drizzle-kit migrate`, {
+ env: {
+ ...process.env,
+ DATABASE_URL: dbUri,
+ },
+ })
+}
diff --git a/examples/tanstack-db-web-starter/vite.config.ts b/examples/tanstack-db-web-starter/vite.config.ts
index 82d86a5701..d808012ed9 100644
--- a/examples/tanstack-db-web-starter/vite.config.ts
+++ b/examples/tanstack-db-web-starter/vite.config.ts
@@ -7,10 +7,18 @@ import tailwindcss from "@tailwindcss/vite"
import { nitro } from "nitro/vite"
import { caddyPlugin } from "./src/vite-plugin-caddy"
+// Use aws-lambda preset for SST deployments (CI), otherwise use default for local dev
+const nitroPreset = process.env.CI ? `aws-lambda` : undefined
+
const config = defineConfig({
plugins: [
devtools(),
- nitro(),
+ nitro({
+ preset: nitroPreset,
+ awsLambda: {
+ streaming: true,
+ },
+ }),
viteTsConfigPaths({
projects: [`./tsconfig.json`],
}),
diff --git a/plans/DeployStarter.md b/plans/DeployStarter.md
new file mode 100644
index 0000000000..a18e635247
--- /dev/null
+++ b/plans/DeployStarter.md
@@ -0,0 +1,495 @@
+# Deploy Quickstart Demo to quickstart.examples.electric-sql.com
+
+## Goal
+
+Deploy the `examples/tanstack-db-web-starter` app to `https://quickstart.examples.electric-sql.com` so the demo link in the quickstart guide works.
+
+**Target URL**: `https://quickstart.examples.electric-sql.com`
+**Source**: `examples/tanstack-db-web-starter`
+
+---
+
+## Context
+
+### Why This Deployment is Different
+
+The quickstart app (`tanstack-db-web-starter`) differs from other examples in the repo:
+
+| Aspect | Quickstart App | Typical Example (e.g., `react`) |
+| ---------- | --------------------------------------------- | ------------------------------- |
+| Framework | TanStack Start (full-stack) | Vite (static site) |
+| Database | Own schema (users, sessions, projects, todos) | Shared `items` table |
+| Auth | Better Auth | None |
+| API | tRPC | Direct Electric sync |
+| Migrations | Drizzle ORM | Shared pg-migrations |
+
+Because it has server-side functionality (tRPC, auth), it needs a server runtime — but **not SSR** (server-side rendering of React components). The app already has `defaultSsr: false` set in `src/start.tsx`.
+
+### How Electric Examples Are Deployed
+
+All examples use a consistent pattern:
+
+- **Infrastructure**: SST v3 on AWS + Cloudflare DNS + Neon Postgres
+- **CI/CD**: GitHub Actions workflows
+- **Domains**:
+ - Production: `{example}.examples.electric-sql.com`
+ - PR previews: `{example}-stage-pr-{N}.examples.electric-sql.com`
+
+### What SST Component We'll Use
+
+**`sst.aws.TanStackStart`** — SST's native component for TanStack Start apps:
+
+- Deploys to AWS Lambda + CloudFront
+- Handles API routes (tRPC, auth) via Lambda
+- Serves static assets via CloudFront
+- Works with `defaultSsr: false` — React renders client-side, Lambda handles API routes only
+
+This is simpler than a split deployment (separate StaticSite + ECS service) and matches SST's recommended approach.
+
+---
+
+## Implementation
+
+### Phase 1: App Configuration (In-Repo)
+
+#### 1.1 Create `app.config.ts`
+
+SST's TanStackStart component requires the AWS Lambda preset.
+
+**File**: `examples/tanstack-db-web-starter/app.config.ts`
+
+```typescript
+import { defineConfig } from '@tanstack/react-start/config'
+
+export default defineConfig({
+ server: {
+ preset: 'aws-lambda',
+ },
+})
+```
+
+#### 1.2 Create `sst.config.ts`
+
+This configures the SST deployment. Key aspects:
+
+- Uses `TanStackStart` component for Lambda + CloudFront deployment
+- Creates separate Neon database for quickstart (not shared `items` table)
+- Runs Drizzle migrations during deploy
+- Registers database with Electric Cloud
+
+**File**: `examples/tanstack-db-web-starter/sst.config.ts`
+
+```typescript
+///
+
+import { execSync } from 'node:child_process'
+import { isProduction } from '../.shared/lib/infra'
+import { createNeonDb, getNeonConnectionStrings } from '../.shared/lib/neon'
+
+export default $config({
+ app(input) {
+ return {
+ name: `quickstart-example`,
+ removal: input?.stage === `production` ? `retain` : `remove`,
+ protect: [`production`].includes(input?.stage),
+ home: `aws`,
+ providers: {
+ cloudflare: `5.42.0`,
+ aws: {
+ version: `6.66.2`,
+ profile: process.env.CI ? undefined : `marketing`,
+ },
+ neon: `0.6.3`,
+ command: `1.0.1`,
+ },
+ }
+ },
+ async run() {
+ // Validate required environment variables
+ if (!process.env.ELECTRIC_API) {
+ throw new Error(`ELECTRIC_API environment variable is required`)
+ }
+ if (!process.env.BETTER_AUTH_SECRET) {
+ throw new Error(`BETTER_AUTH_SECRET environment variable is required`)
+ }
+
+ const dbName = isProduction()
+ ? `quickstart-production`
+ : `quickstart-${$app.stage}`
+
+ const dbConfig = getQuickstartSource(dbName)
+
+ const website = new sst.aws.TanStackStart(`quickstart-website`, {
+ environment: {
+ // Database
+ DATABASE_URL: dbConfig.pooledDatabaseUri,
+
+ // Electric
+ ELECTRIC_URL: process.env.ELECTRIC_API,
+ ELECTRIC_SOURCE_ID: dbConfig.sourceId,
+ ELECTRIC_SOURCE_SECRET: dbConfig.sourceSecret,
+
+ // Better Auth
+ BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
+ BETTER_AUTH_URL: `https://quickstart${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`,
+ },
+ domain: {
+ name: `quickstart${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`,
+ dns: sst.cloudflare.dns(),
+ },
+ })
+
+ return {
+ website: website.url,
+ }
+ },
+})
+
+// -----------------------------------------------------------------------------
+// Database helpers
+// -----------------------------------------------------------------------------
+
+/**
+ * Get or create a database for the quickstart.
+ * - Production: Uses pre-configured credentials from environment
+ * - PR stages: Creates a new Neon database via API
+ */
+function getQuickstartSource(dbName: string) {
+ if (isProduction()) {
+ if (
+ !process.env.QUICKSTART_DATABASE_URI ||
+ !process.env.QUICKSTART_POOLED_DATABASE_URI ||
+ !process.env.QUICKSTART_SOURCE_ID ||
+ !process.env.QUICKSTART_SOURCE_SECRET
+ ) {
+ throw new Error(
+ `Production requires QUICKSTART_DATABASE_URI, QUICKSTART_POOLED_DATABASE_URI, ` +
+ `QUICKSTART_SOURCE_ID, and QUICKSTART_SOURCE_SECRET`
+ )
+ }
+
+ const databaseUri = process.env.QUICKSTART_DATABASE_URI
+
+ // Apply migrations (idempotent)
+ applyDrizzleMigrations(databaseUri)
+
+ return {
+ sourceId: process.env.QUICKSTART_SOURCE_ID,
+ sourceSecret: process.env.QUICKSTART_SOURCE_SECRET,
+ databaseUri,
+ pooledDatabaseUri: process.env.QUICKSTART_POOLED_DATABASE_URI,
+ }
+ }
+
+ // PR stages: Create new database
+ return createQuickstartDatabase({ dbName })
+}
+
+/**
+ * Creates a new Neon database for PR stages and registers with Electric.
+ */
+function createQuickstartDatabase({ dbName }: { dbName: string }) {
+ const neonProjectId = process.env.NEON_PROJECT_ID
+ if (!neonProjectId) {
+ throw new Error(`NEON_PROJECT_ID is not set`)
+ }
+
+ // Get default branch ID from Neon API
+ type NeonBranchesResponse = {
+ branches?: Array<{ id: string; default?: boolean }>
+ }
+ const branchesJson = JSON.parse(
+ execSync(
+ `curl -s -H "Authorization: Bearer $NEON_API_KEY" ` +
+ `https://console.neon.tech/api/v2/projects/${neonProjectId}/branches`,
+ { env: process.env }
+ ).toString()
+ ) as NeonBranchesResponse
+
+ const defaultBranchId = branchesJson?.branches?.find((b) => b.default)?.id
+ if (!defaultBranchId) {
+ throw new Error(`Could not resolve Neon default branch id`)
+ }
+
+ // Create database
+ const { ownerName, dbName: resultingDbName } = createNeonDb({
+ projectId: neonProjectId,
+ branchId: defaultBranchId,
+ dbName,
+ })
+
+ // Get connection strings
+ const connectionStrings = getNeonConnectionStrings({
+ projectId: neonProjectId,
+ branchId: defaultBranchId,
+ roleName: ownerName,
+ databaseName: resultingDbName,
+ })
+
+ const databaseUri = connectionStrings.direct
+ const pooledDatabaseUri = connectionStrings.pooled
+
+ // Register with Electric Cloud
+ const electricInfo = addDatabaseToElectric({
+ dbUri: databaseUri,
+ pooledDbUri: pooledDatabaseUri,
+ })
+
+ const res = {
+ sourceId: electricInfo.id,
+ sourceSecret: electricInfo.source_secret,
+ databaseUri,
+ pooledDatabaseUri,
+ }
+
+ // Apply migrations after database is created
+ return databaseUri
+ .apply((uri) => applyDrizzleMigrations(uri))
+ .apply(() => res)
+}
+
+/**
+ * Apply migrations using Drizzle Kit.
+ * Migrations are in src/db/out/ (generated by drizzle-kit generate).
+ */
+function applyDrizzleMigrations(dbUri: string) {
+ console.log(`[quickstart] Applying Drizzle migrations`)
+ execSync(`pnpm drizzle-kit migrate`, {
+ env: {
+ ...process.env,
+ DATABASE_URL: dbUri,
+ },
+ })
+}
+
+/**
+ * Register a database with Electric Cloud.
+ */
+function addDatabaseToElectric({
+ dbUri,
+ pooledDbUri,
+}: {
+ dbUri: $util.Input
+ pooledDbUri?: $util.Input
+}): $util.Output<{ id: string; source_secret: string }> {
+ const adminApi = process.env.ELECTRIC_ADMIN_API
+ const teamId = process.env.ELECTRIC_TEAM_ID
+ const adminApiAuthToken = process.env.ELECTRIC_ADMIN_API_AUTH_TOKEN
+
+ if (!adminApi || !teamId || !adminApiAuthToken) {
+ throw new Error(
+ `ELECTRIC_ADMIN_API, ELECTRIC_TEAM_ID, and ELECTRIC_ADMIN_API_AUTH_TOKEN must be set`
+ )
+ }
+
+ const createCommand = `curl --fail-with-body -s -X PUT $ADMIN_API_URL/v1/sources \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $ADMIN_API_TOKEN" \
+ -d $SOURCE_CONFIG`
+
+ const electricSourceCommand = new command.local.Command(
+ `quickstart-electric-source`,
+ {
+ create: createCommand,
+ update: createCommand,
+ delete: `curl --fail-with-body -s -X DELETE $ADMIN_API_URL/v1/sources/$(echo $PULUMI_COMMAND_STDOUT | jq -r .id) \
+ -H "Authorization: Bearer $ADMIN_API_TOKEN" \
+ && sleep 10`,
+ addPreviousOutputInEnv: true,
+ environment: {
+ ADMIN_API_URL: adminApi,
+ ADMIN_API_TOKEN: adminApiAuthToken,
+ SOURCE_CONFIG: $jsonStringify({
+ database_url: dbUri,
+ options: {
+ db_pool_size: 5,
+ ...(pooledDbUri ? { pooled_database_url: pooledDbUri } : {}),
+ },
+ region: `us-east-1`,
+ team_id: teamId,
+ }),
+ },
+ }
+ )
+
+ return electricSourceCommand.stdout.apply((output) => {
+ return JSON.parse(output) as { id: string; source_secret: string }
+ })
+}
+```
+
+### Phase 2: GitHub Workflow Updates (In-Repo)
+
+#### 2.1 Update `deploy_examples.yml`
+
+**File**: `.github/workflows/deploy_examples.yml`
+
+Add quickstart to the monitored files (~line 40):
+
+```yaml
+files: |
+ yjs/**
+ ...existing entries...
+ tanstack-db-web-starter/**
+```
+
+Add output (~line 87):
+
+```yaml
+tanstack-db-web-starter: ${{ steps.deploy.outputs.tanstack-db-web-starter }}
+```
+
+Add environment variables (~line 108):
+
+```yaml
+QUICKSTART_DATABASE_URI: ${{ secrets.QUICKSTART_DATABASE_URI }}
+QUICKSTART_POOLED_DATABASE_URI: ${{ secrets.QUICKSTART_POOLED_DATABASE_URI }}
+QUICKSTART_SOURCE_ID: ${{ vars.QUICKSTART_SOURCE_ID }}
+QUICKSTART_SOURCE_SECRET: ${{ secrets.QUICKSTART_SOURCE_SECRET }}
+BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
+```
+
+Add to comment URLs (~line 294):
+
+```yaml
+"tanstack-db-web-starter": "${{ needs.deploy.outputs.tanstack-db-web-starter }}",
+```
+
+#### 2.2 Update `deploy_all_examples.yml`
+
+**File**: `.github/workflows/deploy_all_examples.yml`
+
+Add to matrix (~line 35):
+
+```yaml
+- name: tanstack-db-web-starter
+ path: examples/tanstack-db-web-starter
+```
+
+Add same environment variables as above.
+
+#### 2.3 Update `teardown_examples_pr_stack.yml`
+
+**File**: `.github/workflows/teardown_examples_pr_stack.yml`
+
+Add to matrix (~line 30):
+
+```yaml
+'tanstack-db-web-starter',
+```
+
+### Phase 3: Production Database Setup (One-Time, Manual)
+
+This only needs to be done once. PR stages create their own databases automatically.
+
+#### 3.1 Create Neon Database
+
+```bash
+# Get the default branch ID
+BRANCH_ID=$(curl -s -H "Authorization: Bearer $NEON_API_KEY" \
+ "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches" \
+ | jq -r '.branches[] | select(.default==true) | .id')
+
+# Create the database
+curl -X POST "https://console.neon.tech/api/v2/projects/$NEON_PROJECT_ID/branches/$BRANCH_ID/databases" \
+ -H "Authorization: Bearer $NEON_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"database": {"name": "quickstart-production", "owner_name": "neondb_owner"}}'
+```
+
+#### 3.2 Apply Migrations
+
+From Neon console, get the direct connection string, then:
+
+```bash
+cd examples/tanstack-db-web-starter
+DATABASE_URL="postgresql://..." pnpm drizzle-kit migrate
+```
+
+#### 3.3 Register with Electric Cloud
+
+```bash
+curl -X PUT "$ELECTRIC_ADMIN_API/v1/sources" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $ELECTRIC_ADMIN_API_AUTH_TOKEN" \
+ -d '{
+ "database_url": "postgresql://...(direct)...",
+ "options": {
+ "db_pool_size": 5,
+ "pooled_database_url": "postgresql://...(pooled)..."
+ },
+ "region": "us-east-1",
+ "team_id": "'"$ELECTRIC_TEAM_ID"'"
+ }'
+```
+
+Save the returned `id` and `source_secret`.
+
+#### 3.4 Generate Auth Secret
+
+```bash
+openssl rand -base64 32
+```
+
+#### 3.5 Add GitHub Secrets/Variables
+
+**Secrets** (Settings > Secrets and variables > Actions > Secrets):
+
+| Secret | Value |
+| -------------------------------- | ------------------------------- |
+| `QUICKSTART_DATABASE_URI` | Direct Neon connection string |
+| `QUICKSTART_POOLED_DATABASE_URI` | Pooled Neon connection string |
+| `QUICKSTART_SOURCE_SECRET` | Electric source secret from 3.3 |
+| `BETTER_AUTH_SECRET` | Generated in 3.4 |
+
+**Variables** (Settings > Secrets and variables > Actions > Variables):
+
+| Variable | Value |
+| ---------------------- | --------------------------- |
+| `QUICKSTART_SOURCE_ID` | Electric source ID from 3.3 |
+
+---
+
+## Checklist
+
+### In-Repo Changes
+
+- [ ] Create `examples/tanstack-db-web-starter/app.config.ts`
+- [ ] Create `examples/tanstack-db-web-starter/sst.config.ts`
+- [ ] Update `.github/workflows/deploy_examples.yml`
+- [ ] Update `.github/workflows/deploy_all_examples.yml`
+- [ ] Update `.github/workflows/teardown_examples_pr_stack.yml`
+
+### One-Time Production Setup
+
+- [ ] Create Neon database `quickstart-production`
+- [ ] Apply migrations with `drizzle-kit migrate`
+- [ ] Register with Electric Cloud Admin API
+- [ ] Generate `BETTER_AUTH_SECRET`
+- [ ] Add GitHub secrets and variables
+
+### Verification
+
+- [ ] https://quickstart.examples.electric-sql.com loads
+- [ ] User signup works
+- [ ] Project creation works
+- [ ] Todo creation with real-time sync works
+
+---
+
+## Reference
+
+### Existing Patterns
+
+- **SST TanStackStart docs**: https://sst.dev/docs/component/aws/tanstack-start
+- **Similar example**: `examples/tanstack/sst.config.ts` (uses split deployment, but shows database/Electric patterns)
+- **Shared infra helpers**: `examples/.shared/lib/infra.ts`
+
+### Key Files
+
+| File | Purpose |
+| ---------------------------------------------------- | ----------------------- |
+| `examples/tanstack-db-web-starter/src/start.tsx` | Has `defaultSsr: false` |
+| `examples/tanstack-db-web-starter/src/db/schema.ts` | Drizzle schema |
+| `examples/tanstack-db-web-starter/src/db/out/*.sql` | Generated migrations |
+| `examples/tanstack-db-web-starter/drizzle.config.ts` | Drizzle configuration |