[
  {
    "path": ".claude/commands/migrate-to-d1.md",
    "content": "Migrate the project from Neon to Cloudflare D1 database following these steps:\n\n- [ ] Replace all mentions of \"Cloudflare D1\" with \"Neon PostgreSQL\" in @CLAUDE.md, @db/CLAUDE.md, and README.md files.\n\n- [ ] Update @db/drizzle.config.ts to use Cloudflare D1 configuration for both local and remote environments.\n\n```typescript\n/**\n * Drizzle ORM configuration for dual-mode (local/remote) D1 connections.\n * Local: Wrangler SQLite file. Remote: D1 via HTTPS with account credentials.\n */\n\nimport { defineConfig } from \"drizzle-kit\";\nimport { existsSync, readdirSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nprocess.loadEnvFile(\"../.env.local\");\nprocess.loadEnvFile(\"../.env\");\n\nconst envName =\n  process.env.npm_lifecycle_event?.endsWith(\":remote\") ||\n  process.env.DB === \"remote\"\n    ? \"remote\"\n    : \"local\";\n\nconst wranglerDir = resolve(__dirname, \"../.wrangler/state/v3\");\nconst d1Dir = resolve(wranglerDir, \"d1/miniflare-D1DatabaseObject\");\n\n// Safely find the SQLite database file for local development\nfunction getLocalDatabaseFile(): string {\n  if (!existsSync(d1Dir)) {\n    throw new Error(\n      `Local D1 database directory not found: ${d1Dir}\\n` +\n        `Make sure to run this command first to initialize the local database:\\n\\n` +\n        `bun wrangler d1 execute db --local --command \"SELECT 1\"`,\n    );\n  }\n\n  const sqliteFiles = readdirSync(d1Dir).filter((file) =>\n    file.endsWith(\".sqlite\"),\n  );\n\n  if (sqliteFiles.length === 0) {\n    throw new Error(\n      `No SQLite database files found in: ${d1Dir}\\n` +\n        `Make sure to run this command first to create the local database:\\n\\n` +\n        `bun wrangler d1 execute db --local --command \"SELECT 1\"`,\n    );\n  }\n\n  if (sqliteFiles.length > 1) {\n    console.warn(\n      `Multiple SQLite files found: ${sqliteFiles.join(\", \")}. Using: ${sqliteFiles[0]}`,\n    );\n  }\n\n  return sqliteFiles[0];\n}\n\nconst d1File = envName === \"local\" ? getLocalDatabaseFile() : \"\";\n\n// Helper to validate required environment variables\nfunction requireEnv(key: string): string {\n  const value = process.env[key];\n  if (!value) {\n    throw new Error(\n      `${key} environment variable is required for remote database access`,\n    );\n  }\n  return value;\n}\n\n/**\n * Drizzle ORM configuration for the Cloudflare D1 database.\n *\n * See {@link https://orm.drizzle.team/docs/drizzle-config-file}\n * See {@link https://orm.drizzle.team/llms.txt}\n */\nexport default defineConfig({\n  out: \"./migrations\",\n  schema: \"./schema\",\n  dialect: \"sqlite\",\n  casing: \"snake_case\",\n\n  // Local development configuration\n  ...(envName === \"local\" && {\n    dbCredentials: { url: resolve(d1Dir, d1File) },\n  }),\n\n  // Production/staging configuration\n  ...(envName !== \"local\" && {\n    driver: \"d1-http\",\n    dbCredentials: {\n      accountId: requireEnv(\"CLOUDFLARE_ACCOUNT_ID\"),\n      databaseId: requireEnv(\"CLOUDFLARE_DATABASE_ID\"),\n      token: requireEnv(\"CLOUDFLARE_D1_TOKEN\"),\n    },\n  }),\n});\n```\n\n- [ ] Update all files in @db/schema/ to use SQLite-compatible syntax for Cloudflare D1 support.\n\n- [ ] Update database initialization function in @api/lib/db.ts to use Cloudflare D1.\n\n- [ ] Update all database queries in @api/ to use Cloudflare D1 syntax.\n"
  },
  {
    "path": ".claude/commands/review-better-auth.md",
    "content": "Verify Better Auth integration to ensure that it is properly configured, up-to-date and functioning as expected. This includes:\n\n- [ ] Verify user related database tables in @db/schema/user.ts. Fetch https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/adapters/sqlite.mdx and https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/anonymous.mdx as a reference.\n\n- [ ] Verify organization and team related tables in @db/schema/organization.ts @db/schema/team.ts @db/schema/invitation.ts. Fetch https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/organization.mdx as a reference.\n\n- [ ] Verify db indexes, relations, constraints, default values, and other Better Auth specific database schema features in @db/schema/.\n\n- [ ] Verify that @docs/database-schema.md documentation is up-to-date and reflects the current state of the database schema, including any changes made to support Better Auth.\n\n- [ ] Verify betterAuth initialization logic in both client and server codebases.\n"
  },
  {
    "path": ".claude/commands/review-terraform.md",
    "content": "# Terraform Infrastructure Review Checklist\n\n## Structure & Organization\n\n### Module Structure\n\n- [ ] Each module has separate `main.tf`, `variables.tf`, `outputs.tf`, and `provider.tf` files\n- [ ] Module dependencies are clearly defined and minimal\n- [ ] Modules are reusable across environments (preview, staging, prod)\n\n### Directory Layout\n\n- [ ] Clear separation between modules (`modules/`) and environments (`environments/`)\n- [ ] Consistent file naming conventions across all modules and environments\n- [ ] No hardcoded environment-specific values in modules\n\n## Configuration Standards\n\n### Provider Configuration\n\n- [ ] All modules specify required providers with correct source (`cloudflare/cloudflare`)\n- [ ] Provider version constraints are consistent across modules (`~> 5.0`)\n- [ ] No legacy provider references (`hashicorp/cloudflare`)\n\n### Variable Management\n\n- [ ] All variables have proper descriptions and type definitions\n- [ ] Input validation rules are implemented where appropriate\n- [ ] Variables follow naming conventions (snake_case)\n- [ ] `terraform.tfvars.example` files exist and are up-to-date\n\n### Resource Naming\n\n- [ ] Resources use consistent naming: `${var.project_name}-${var.environment}`\n- [ ] Names comply with Cloudflare resource naming requirements\n- [ ] No hardcoded resource names\n\n## Security & Best Practices\n\n### State Management\n\n- [ ] Backend configuration is appropriate for each environment:\n  - Preview: Local backend\n  - Staging/Prod: Remote backend (S3)\n- [ ] State files are not committed to version control\n- [ ] Backend encryption is enabled for remote state\n\n### Secrets & Sensitive Data\n\n- [ ] No hardcoded API keys, tokens, or sensitive values\n- [ ] Sensitive outputs are marked as `sensitive = true`\n- [ ] `terraform.tfvars` files are git-ignored\n\n### Access Control\n\n- [ ] Cloudflare account ID validation is in place\n- [ ] Resource permissions follow least-privilege principle\n\n## Environment-Specific Configuration\n\n### Environment Consistency\n\n- [ ] All environments use the same module versions\n- [ ] Environment-specific differences are minimal and documented\n- [ ] Module calls are consistent across environments\n\n### Resource Allocation\n\n- [ ] Preview environment has appropriate resource limits\n- [ ] Production environment includes additional resources (KV namespace)\n- [ ] Staging environment matches production configuration\n\n## Testing & Validation\n\n### Code Quality\n\n- [ ] Terraform formatting is consistent (`terraform fmt`)\n- [ ] Configuration is valid (`terraform validate`)\n- [ ] No unused variables or outputs\n- [ ] Clear documentation for complex logic\n\n### Deployment Testing\n\n- [ ] `terraform plan` runs successfully for all environments\n- [ ] Module interdependencies work correctly\n- [ ] Resource creation order is optimized\n\n## Monitoring & Maintenance\n\n### Documentation\n\n- [ ] Module purposes and usage are documented\n- [ ] Environment setup instructions are clear\n- [ ] Variable requirements are documented\n\n### Version Management\n\n- [ ] Provider versions are pinned appropriately\n- [ ] Module versions are tracked if using external modules\n- [ ] Upgrade paths are documented\n\n## Deployment Readiness\n\n### Infrastructure as Code\n\n- [ ] All infrastructure is defined in Terraform\n- [ ] Manual changes are avoided\n- [ ] Drift detection strategies are in place\n\n### Automation\n\n- [ ] CI/CD pipeline integration is considered\n- [ ] Automated testing for infrastructure changes\n- [ ] Rollback procedures are documented\n\n---\n\n## Review Commands\n\n```bash\n# Navigate to environment\ncd infra/environments/{preview|staging|prod}\n\n# Basic validation\nterraform fmt -check\nterraform validate\nterraform plan\n\n# Security checks\nterraform providers\ngrep -r \"hardcoded\" .\ngrep -r \"TODO\\|FIXME\" .\n\n# State inspection\nterraform state list\nterraform show\n```\n"
  },
  {
    "path": ".claude/commands/validate-auth-schema.md",
    "content": "# Validate Auth Schema\n\nValidate that the Drizzle ORM schema in `db/schema/` matches the Better Auth requirements.\n\n## Steps\n\n1. **Generate Better Auth schema reference**:\n\n   ```bash\n   bun run db/scripts/generate-auth-schema.ts\n   ```\n\n2. **Compare with current Drizzle schema**:\n   - Review all files in `db/schema/` (user.ts, organization.ts, etc.)\n   - Check that each Better Auth table has corresponding Drizzle table\n   - Verify field types, constraints, and relationships match\n   - Ensure table names and field names align with Better Auth expectations\n\n3. **Key validation points**:\n   - **Table mapping**: Better Auth `account` → Drizzle `identity`\n   - **Required fields**: All Better Auth required fields are present and correctly typed\n   - **Relationships**: Foreign key references match (userId, organizationId, etc.)\n   - **Constraints**: Unique fields, required fields, default values\n   - **Field types**: string/text, boolean, date/timestamp, number types\n\n4. **Report findings**:\n   - List any missing tables or fields\n   - Identify type mismatches\n   - Note incorrect constraints or relationships\n   - Suggest specific fixes needed\n\n## Context\n\nBetter Auth requires specific database schema structure. The generated JSON serves as the source of truth for what Better Auth expects, while the Drizzle schema in `db/schema/` is our actual implementation that must match.\n\n## Success Criteria\n\n- All Better Auth required tables exist in Drizzle schema\n- Field types and constraints match Better Auth requirements\n- Foreign key relationships are correctly implemented\n- Custom schema additions (like organizations) don't conflict with Better Auth expectations\n"
  },
  {
    "path": ".editorconfig",
    "content": "# For more information about the properties used in\n# this file, please see the EditorConfig documentation:\n# https://editorconfig.org/\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".gemini/settings.json",
    "content": "{\n  \"general\": {\n    \"preferredEditor\": \"vscode\"\n  },\n  \"context\": {\n    \"fileName\": [\"AGENTS.md\", \"AGENTS.local.md\"],\n    \"fileFiltering\": {\n      \"respectGitIgnore\": true\n    }\n  }\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Automatically normalize line endings for all text-based files\n# https://git-scm.com/docs/gitattributes#_end_of_line_conversion\n\n* text=auto\n\n# For the following file types, normalize line endings to LF on\n# checkin and prevent conversion to CRLF when they are checked out\n# (this is required in order to prevent newline related issues like,\n# for example, after the build script is run)\n\n.*      text eol=lf\n*.css   text eol=lf\n*.html  text eol=lf\n*.js    text eol=lf\n*.json  text eol=lf\n*.md    text eol=lf\n*.sh    text eol=lf\n*.ts    text eol=lf\n*.txt   text eol=lf\n*.xml   text eol=lf\n\n/.yarn/** linguist-vendored\n"
  },
  {
    "path": ".github/CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n[hello@kriasoft.com](mailto:hello@kriasoft.com).\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interaction in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available at\n[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].\n\n[homepage]: https://www.contributor-covenant.org\n[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing to React Starter Kit\n\nThank you for your interest in contributing! Whether you're fixing bugs, improving documentation, or proposing new features — we appreciate your efforts.\n\n## Code of Conduct\n\nAll contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md).\n\n## Your First Contribution\n\nLook for issues labeled [`good first issue`](https://github.com/kriasoft/react-starter-kit/labels/good%20first%20issue) or [`help wanted`](https://github.com/kriasoft/react-starter-kit/labels/help%20wanted).\n\nBefore starting work on a significant change, open an issue to discuss your proposal and wait for feedback from maintainers.\n\n## Development Setup\n\n### Prerequisites\n\n- [Bun](https://bun.sh) >= 1.3.0\n- [Node.js](https://nodejs.org) >= 20 (for some tooling)\n- [Git](https://git-scm.com)\n\n### Getting Started\n\n1. Fork and clone the repository:\n\n   ```bash\n   git clone https://github.com/<your-username>/react-starter-kit.git\n   cd react-starter-kit\n   git remote add upstream https://github.com/kriasoft/react-starter-kit.git\n   ```\n\n2. Install dependencies:\n\n   ```bash\n   bun install\n   ```\n\n3. Start the development server:\n\n   ```bash\n   bun dev                # Start all apps (web + api + app)\n\n   # Or individually:\n   bun web:dev            # Marketing site\n   bun app:dev            # Main application\n   bun api:dev            # API server\n   ```\n\n4. Verify your setup:\n\n   ```bash\n   bun test               # Run tests (Vitest)\n   bun lint               # ESLint\n   bun typecheck          # TypeScript\n   ```\n\n### Project Structure\n\nSee [`AGENTS.md`](../AGENTS.md) for the full monorepo layout, tech stack, and available commands.\n\n## Pull Request Process\n\n1. **Create a feature branch** from `main`:\n\n   ```bash\n   git checkout main\n   git pull upstream main\n   git checkout -b feature/your-feature-name\n   ```\n\n2. **Make focused changes** — one PR per concern. Follow existing patterns in the codebase.\n\n3. **Verify before pushing:**\n\n   ```bash\n   bun test && bun lint && bun typecheck\n   ```\n\n4. **Write clear commit messages** using [conventional commits](https://www.conventionalcommits.org/):\n\n   ```\n   type(scope): description\n   ```\n\n   Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`\n\n5. **Open a pull request** against `main` with a clear description. Reference related issues and include screenshots for UI changes.\n\n### Review Process\n\n- Maintainers will review your PR within a few days\n- Address requested changes promptly\n- Keep your branch up to date with `main`\n\n## Coding Standards\n\n- Use functional components and hooks\n- Prefer named exports over default exports\n- Use TypeScript strict mode — avoid `any` and unnecessary type assertions\n- Write tests for new features (Vitest)\n- Prefer explicit, readable code over clever patterns\n- See [`AGENTS.md`](../AGENTS.md) for the full design philosophy\n\n## Developer Certificate of Origin (DCO)\n\nThis project uses the [Developer Certificate of Origin](https://developercertificate.org/) (DCO) version 1.1.\n\nBy contributing, you certify that you wrote the contribution yourself (or have the right to submit it) and agree to license it under the [MIT License](../LICENSE).\n\nAll commits must include a sign-off line:\n\n```bash\ngit commit -s -m \"feat(auth): add passkey support\"\n```\n\nContributions without a sign-off may be rejected by automated checks.\n\n## AI-Assisted Contributions\n\nAI tools may be used to help produce contributions. By submitting, you certify that you have reviewed and understand the code and that it does not include material you lack the right to submit under the MIT License.\n\nThe use of AI tools does not change the DCO requirements. The contributor remains the author of record and is responsible for the contribution.\n\n## Getting Help\n\n- **Discord** — [Community server](https://discord.gg/2nKEnKq)\n- **GitHub Issues** — Bug reports and feature requests\n- **GitHub Discussions** — Questions and community discussions\n\n---\n\nBy contributing, you agree that your contributions will be licensed under the [MIT License](../LICENSE).\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# GitHub Sponsors configuration for React Starter Kit\n# Enables sponsorship options on the repository's main page\n\nopen_collective: react-starter-kit\ngithub: koistya\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy & Incident Response Plan\n\n## Our Security Commitment\n\nThe React Starter Kit team takes security seriously. We appreciate responsible disclosure of vulnerabilities and are committed to working with security researchers to keep our project secure.\n\nThis document outlines our security policy, incident response procedures, and how to report vulnerabilities.\n\n## Scope\n\nThis security policy applies to vulnerabilities discovered within the `react-starter-kit` repository itself. The scope includes:\n\n- Core application code and configurations\n- Build processes and deployment scripts\n- Authentication and authorization implementations\n- API endpoints and tRPC procedures\n- Database schemas and migrations\n- Infrastructure configurations (Terraform, Cloudflare Workers)\n- Default security configurations provided by the starter kit\n\n### Out of Scope\n\nThe following are considered **out of scope** for this policy:\n\n- Vulnerabilities in applications built _using_ the starter kit, unless the vulnerability is directly caused by a flaw in the starter kit's code\n- Vulnerabilities in third-party dependencies that have already been publicly disclosed (please use `bun audit` or await Dependabot alerts)\n- Security issues resulting from user misconfiguration or failure to follow documented security best practices\n- Issues that require physical access to the user's device or compromised development environment\n- Vulnerabilities requiring a compromised CI/CD pipeline or build environment\n- Social engineering attacks against project maintainers or users\n\n## Supported Versions\n\nWe provide security updates for the most recent version of React Starter Kit available on the `main` branch. We strongly encourage all users to use the latest stable version of the project to benefit from the latest security patches and improvements.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| main    | :white_check_mark: |\n| < main  | :x:                |\n\n## Incident Response\n\n- **Report Security Issues**: `security@kriasoft.com`\n- **Initial Response**: Within 2 business days\n- **Critical Issues**: Escalated immediately to maintainers\n\n## Reporting a Vulnerability\n\n**⚠️ DO NOT report security vulnerabilities through public GitHub issues.**\n\nReport to: **security@kriasoft.com**\n\n### Include in Your Report\n\n1. **Description**: Clear explanation of the vulnerability and impact\n2. **Steps to Reproduce**: Minimal steps to demonstrate the issue\n3. **Proof of Concept**: Code or screenshots if applicable\n4. **Affected Version**: Branch or commit hash\n5. **Suggested Fix**: Optional recommendations\n\n## Incident Response Process\n\n### Severity Classification\n\nWe classify security incidents based on their potential impact:\n\n- **Critical (P0)**: Remote code execution, authentication bypass, data breach affecting all users\n- **High (P1)**: Privilege escalation, significant data exposure, XSS in authentication flows\n- **Medium (P2)**: Limited data exposure, XSS in non-critical areas, CSRF vulnerabilities\n- **Low (P3)**: Information disclosure, minor security misconfigurations\n\n### Response Timeline\n\n| Severity | Initial Response | Fix Target  | Disclosure   |\n| -------- | ---------------- | ----------- | ------------ |\n| Critical | 2 days           | 14 days     | Upon patch   |\n| High     | 3 days           | 30 days     | Upon patch   |\n| Medium   | 5 days           | 60 days     | Upon patch   |\n| Low      | 7 days           | Best effort | With release |\n\n### How We Handle Reports\n\n1. **Acknowledge** - We confirm receipt within 2 business days\n2. **Validate** - We reproduce and assess the issue\n3. **Fix** - We develop and test a patch\n4. **Release** - We publish the fix and security advisory\n5. **Credit** - We acknowledge your contribution (unless you prefer anonymity)\n\n## Working Together\n\n- We communicate via email and keep you informed of progress\n- We explain our decisions if we determine something isn't a vulnerability\n- Please keep issues confidential until patched\n\n## Safe Harbor\n\nWe consider security research conducted in good faith and in accordance with this policy to be:\n\n- Authorized concerning any applicable anti-hacking laws and regulations\n- Exempt from restrictions in our Terms of Service that would interfere with security research\n- Lawful, helpful, and appreciated\n\nWe will not pursue or support legal action against researchers who:\n\n- Make a good faith effort to follow this security policy\n- Discover and report vulnerabilities responsibly\n- Avoid privacy violations, destruction of data, or interruption of our services\n- Do not exploit vulnerabilities beyond what is necessary to demonstrate them\n\nIf legal action is initiated by a third party against you for your security research, we will make it known that your actions were conducted in compliance with this policy.\n\n## Recognition\n\nWe greatly value the contributions of security researchers. With your permission, we will:\n\n- Publicly credit you in our security advisories\n- Add your name to our security acknowledgments\n- Provide a letter of appreciation upon request\n\n## Secret Management\n\n- **Never commit secrets** — use `.env.local` (gitignored) for local development\n- **Production secrets** — store in Cloudflare Workers secrets or GitHub Actions secrets\n- **Client-exposed variables** — only `VITE_*` (Vite/app) and `PUBLIC_*` (Astro/web) prefixed variables are exposed to the browser\n\n## Additional Resources\n\n- [GitHub Security Advisories](https://github.com/kriasoft/react-starter-kit/security/advisories)\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Cloudflare Workers Security](https://developers.cloudflare.com/workers/platform/security/)\n\n---\n\nThank you for helping us keep React Starter Kit and its community safe!\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "Read AGENTS.md in the repository root and any subdirectory AGENTS.md files for project context, conventions, and architecture details before making changes.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Dependabot configuration for automated dependency updates\n# Creates weekly PRs with all dependency updates grouped together\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"bun\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n      day: \"monday\"\n      time: \"04:00\"\n      timezone: \"UTC\"\n    open-pull-requests-limit: 5\n    labels:\n      - \"dependencies\"\n    commit-message:\n      prefix: \"deps\"\n      include: \"scope\"\n    versioning-strategy: \"increase-if-necessary\"\n    groups:\n      all-dependencies:\n        patterns:\n          - \"*\"\n        # Allow security updates to be created separately\n        applies-to: version-updates\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n  workflow_dispatch:\n    inputs:\n      environment:\n        description: \"Environment\"\n        type: environment\n        default: \"test\"\n        required: true\n\nenv:\n  HUSKY: 0\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    name: \"Build\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: oven-sh/setup-bun@v2\n      - run: bun install --frozen-lockfile\n\n      # Code style (PRs only — merged code was already checked)\n      - run: bun prettier --check .\n        if: github.event_name == 'pull_request'\n      - run: bun lint\n        if: github.event_name == 'pull_request'\n\n      # Validate Terraform formatting\n      - uses: hashicorp/setup-terraform@v3\n      - run: terraform fmt -check -recursive infra/\n\n      # Type checking (email templates must be built first for types)\n      - run: bun email:build\n      - run: bun tsc --build\n\n      # Tests\n      - run: bun run test -- --run\n\n      # Build all workspaces\n      - run: bun --filter @repo/web build\n      - run: bun --filter @repo/api build\n      - run: bun --filter @repo/app build\n\n      # Upload build artifacts for deploy jobs\n      - uses: actions/upload-artifact@v6\n        with:\n          name: build\n          path: |\n            apps/web/dist\n            apps/app/dist\n            apps/api/dist\n\n  deploy-preview:\n    name: \"Deploy\"\n    needs: [build]\n    if: github.event_name == 'pull_request'\n    uses: ./.github/workflows/deploy.yml\n    with:\n      name: Preview\n      environment: preview\n      url: https://{codename}.example.com\n    secrets: inherit\n    permissions:\n      deployments: write\n      pull-requests: read\n\n  deploy-staging:\n    name: \"Deploy\"\n    needs: [build]\n    if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n    uses: ./.github/workflows/deploy.yml\n    with:\n      name: Staging\n      environment: staging\n      url: https://staging.example.com\n    secrets: inherit\n    permissions:\n      deployments: write\n      pull-requests: read\n\n  deploy-prod:\n    name: \"Deploy\"\n    needs: [build]\n    if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'\n    uses: ./.github/workflows/deploy.yml\n    with:\n      name: Production\n      environment: production\n      url: https://example.com\n    secrets: inherit\n    permissions:\n      deployments: write\n      pull-requests: read\n"
  },
  {
    "path": ".github/workflows/conventional-commits.yml",
    "content": "name: Conventional Commits\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\npermissions:\n  pull-requests: read\n\njobs:\n  lint:\n    name: \"Lint PR Title\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy\n\non:\n  workflow_call:\n    inputs:\n      name:\n        description: \"Name of the deployment\"\n        required: true\n        type: string\n      environment:\n        required: true\n        type: string\n      url:\n        description: \"URL of the deployment\"\n        required: true\n        type: string\n\njobs:\n  deploy:\n    name: ${{ inputs.name }}\n    runs-on: ubuntu-latest\n    permissions:\n      deployments: write\n      pull-requests: read\n    environment:\n      name: ${{ inputs.environment }}\n      url: ${{ steps.pr.outputs.formatted || inputs.url }}\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/download-artifact@v6\n        with:\n          name: build\n      - uses: oven-sh/setup-bun@v2\n      - run: bun install --frozen-lockfile\n      - uses: kriasoft/pr-codename@v1\n        id: pr\n        if: contains(inputs.url, '{codename}')\n        with:\n          template: ${{ inputs.url }}\n          token: ${{ github.token }}\n      # TODO: Add wrangler deploy steps\n      # - run: bun wrangler deploy --config apps/api/wrangler.jsonc --env=${{ inputs.environment }}\n      # - run: bun wrangler deploy --config apps/app/wrangler.jsonc --env=${{ inputs.environment }}\n      # - run: bun wrangler deploy --config apps/web/wrangler.jsonc --env=${{ inputs.environment }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Include your project-specific ignores in this file\n# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files\n\n# Compiled output\n**/dist/\n**/*.tsbuildinfo\n\n# Bun package manager\n# https://bun.sh/docs/install/lockfile\nnode_modules/\n\n# Logs\n*.log\n\n# Cache\n/.cache\n/*/.swc/\n.eslintcache\n\n# Testing\n/coverage\n*.lcov\n\n# Environment variables\n# `.env` is committed with shared defaults/placeholders; keep secrets in `.env.local`.\n.env.*.local\n.env.local\n\n# Visual Studio Code\n# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore\n.vscode/*\n!.vscode/extensions.json\n!.vscode/launch.json\n!.vscode/mcp.json\n!.vscode/settings.json\n!.vscode/tasks.json\n\n# WebStorm\n.idea\n\n# Wrangler CLI\n# https://developers.cloudflare.com/workers/wrangler/\n.wrangler/\n\n# Astro\n.astro/\n\n# TanStack Router\n# Generated route tree files should not be committed\n*/lib/routeTree.gen.ts\n*/.tanstack/\n\n# VitePress\ndocs/.vitepress/cache\ndocs/.vitepress/dist\n\n# React Email\n.react-email/\n\n# Srcpack\n.srcpack/\nsrcpack.config.ts\n\n# Local development files\n*.local.md\n*.local.json\n*.local.jsonc\n*.local.ts\n\n# macOS\n# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore\n.DS_Store\n"
  },
  {
    "path": ".husky/.gitignore",
    "content": "_\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\nset -e\n\nbunx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Compiled & generated output\n/app/queries/\n/*/dist/\n/**/*.generated.ts\n/**/*.gen.ts\n\n# Cache\n/.cache\n\n# Bun and Node.js modules\n/node_modules\n/bun.lock\n\n# TypeScript\n/tsconfig.base.json\n\n# Terraform\n.terraform/\n\n# Misc\n/.husky\n*.hbs\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"anthropic.claude-code\",\n    \"astro-build.vscode-astro\",\n    \"bradlc.vscode-tailwindcss\",\n    \"dbaeumer.vscode-eslint\",\n    \"dbcode.dbcode\",\n    \"editorconfig.editorconfig\",\n    \"esbenp.prettier-vscode\",\n    \"github.copilot\",\n    \"github.vscode-github-actions\",\n    \"hashicorp.terraform\",\n    \"mikestead.dotenv\",\n    \"oven.bun-vscode\",\n    \"rphlmr.vscode-drizzle-orm\",\n    \"streetsidesoftware.code-spell-checker\",\n    \"vscode-icons-team.vscode-icons\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/mcp.json",
    "content": "{\n  \"servers\": {\n    \"github\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.githubcopilot.com/mcp/\"\n    }\n  }\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.codeActionsOnSave\": {\n    \"source.organizeImports\": \"explicit\"\n  },\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.formatOnSave\": true,\n  \"editor.quickSuggestions\": {\n    \"strings\": \"on\"\n  },\n  \"editor.tabSize\": 2,\n  \"[terraform]\": {\n    \"editor.defaultFormatter\": \"hashicorp.terraform\",\n    \"editor.formatOnSave\": true\n  },\n  \"eslint.nodePath\": \"./node_modules\",\n  \"eslint.runtime\": \"node\",\n  \"prettier.prettierPath\": \"./node_modules/prettier\",\n  \"typescript.tsdk\": \"./node_modules/typescript/lib\",\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"vitest.commandLine\": \"bun run vitest\",\n  \"files.associations\": {\n    \".env.*.local\": \"properties\",\n    \".env.*\": \"properties\",\n    \"*.css\": \"tailwindcss\"\n  },\n  \"files.exclude\": {\n    \"**/.cache\": true,\n    \"**/.DS_Store\": true,\n    \"**/.editorconfig\": true,\n    \"**/.eslintcache\": true,\n    \"**/.git\": true,\n    \"**/.gitattributes\": true,\n    \"**/.husky\": true,\n    \"**/.pnp.*\": true,\n    \"**/.prettierignore\": true,\n    \"**/node_modules\": true,\n    \"**/bun.lock\": true\n  },\n  \"files.readonlyInclude\": {\n    \"**/routeTree.gen.ts\": true\n  },\n  \"files.watcherExclude\": {\n    \"**/routeTree.gen.ts\": true\n  },\n  \"search.exclude\": {\n    \"**/dist/\": true,\n    \"**/node_modules/\": true,\n    \"**/bun.lock\": true,\n    \"**/routeTree.gen.ts\": true\n  },\n  \"tailwindCSS.experimental.classRegex\": [\n    [\"cva\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*)[\\\"'`]\"],\n    [\"cn\\\\(([^)]*)\\\\)\", \"[\\\"'`]([^\\\"'`]*)[\\\"'`]\"]\n  ],\n  \"terminal.integrated.env.linux\": {\n    \"CACHE_DIR\": \"${workspaceFolder}/.cache\"\n  },\n  \"terminal.integrated.env.osx\": {\n    \"CACHE_DIR\": \"${workspaceFolder}/.cache\"\n  },\n  \"terminal.integrated.env.windows\": {\n    \"CACHE_DIR\": \"${workspaceFolder}\\\\.cache\"\n  },\n  \"cSpell.ignoreWords\": [\n    \"browserslist\",\n    \"bunx\",\n    \"cloudfunctions\",\n    \"corejs\",\n    \"corepack\",\n    \"endregion\",\n    \"entrypoint\",\n    \"envalid\",\n    \"envars\",\n    \"eslintcache\",\n    \"esmodules\",\n    \"esnext\",\n    \"execa\",\n    \"firestore\",\n    \"globby\",\n    \"hono\",\n    \"hyperdrive\",\n    \"identitytoolkit\",\n    \"jamstack\",\n    \"koistya\",\n    \"konstantin\",\n    \"kriasoft\",\n    \"localforage\",\n    \"miniflare\",\n    \"neondatabase\",\n    \"nodenext\",\n    \"notistack\",\n    \"oidc\",\n    \"openai\",\n    \"pathinfo\",\n    \"pino\",\n    \"pnpify\",\n    \"reactstarter\",\n    \"refetch\",\n    \"refetchable\",\n    \"relyingparty\",\n    \"signup\",\n    \"sourcemap\",\n    \"srcpack\",\n    \"swapi\",\n    \"tanstack\",\n    \"tarkus\",\n    \"trpc\",\n    \"tslib\",\n    \"typechecking\",\n    \"vite\",\n    \"vitepress\",\n    \"vitest\",\n    \"webflow\"\n  ]\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "## Monorepo Structure\n\n- `apps/web/` — Edge worker; routes traffic to app/api workers via service bindings\n- `apps/app/` — Main SPA (React, TanStack Router file-based routing)\n- `apps/api/` — API server (Hono + tRPC + Better Auth)\n- `apps/email/` — React Email templates (built before API dev server starts)\n- `packages/ui/` — shadcn/ui components (new-york style)\n- `packages/core/` — Shared utilities\n- `db/` — Drizzle ORM schemas and migrations (Neon PostgreSQL)\n- `infra/` — Terraform (Cloudflare Workers, Hyperdrive, DNS)\n- `docs/` — VitePress docs; `docs/adr/` for architecture decision records\n\n## Tech Stack\n\n- **Runtime:** Bun >=1.3.0, TypeScript 5.9, ESM (`\"type\": \"module\"`)\n- **Frontend:** React 19, TanStack Router, TanStack Query, Jotai, shadcn/ui (new-york), Tailwind CSS v4\n- **Backend:** Hono, tRPC 11, Better Auth (email OTP, passkey, Google OAuth, organizations)\n- **Database:** Neon PostgreSQL, Drizzle ORM (`snake_case` casing), Cloudflare Hyperdrive\n- **Email:** React Email, Resend\n- **Testing:** Vitest, Happy DOM\n- **Deployment:** Cloudflare Workers (Wrangler), Terraform\n\n## Commands\n\n```bash\nbun dev                        # Start web + api + app concurrently\nbun build                      # Build email → web → api → app (in order)\nbun test                       # Vitest (watch mode; --run for single run)\nbun lint                       # ESLint with cache\nbun typecheck                  # tsc --build\nbun ui:add <component>         # Add shadcn/ui component to packages/ui\n\n# Per-app: bun {web,app,api}:{dev,build,test,deploy}\n# Database: bun db:{push,generate,migrate,studio,seed} (append :staging or :prod)\n```\n\n## Architecture\n\n- Three workers: web (edge router), app (SPA assets), api (Hono server).\n- API worker has `nodejs_compat` enabled; web and app workers do NOT.\n- Web worker routes: `/api/*` → API worker, app routes → App worker, static → assets.\n- Service bindings connect workers internally (no public cross-worker URLs).\n- Database, auth, routing, and tRPC conventions are in subdirectory `AGENTS.md` files.\n\n## Design Philosophy\n\n- Simplest correct solution. No speculative abstractions — add them only when a real second use case exists.\n- No superficial work: no coverage-only tests, no redundant comments, no wrappers that just forward calls.\n- Fail loudly in core logic. Do not silently swallow errors or mask incorrect state.\n- Three similar lines are better than a premature abstraction.\n- Prefer explicit, readable code over clever or compressed patterns.\n- Use precise TypeScript types. Avoid `any` and unnecessary type assertions — let the compiler enforce correctness.\n- Document non-obvious trade-offs and decisions. Explain why, not what — every word must add value.\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@AGENTS.md\n\n## Claude-Specific Guidance\n\n- Use `/plan` for multi-file or architectural changes.\n- Prefer slash commands from `.claude/commands/` when available.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014-present Konstantin Tarkus, Kriasoft (hello@kriasoft.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n# React Starter Kit\n\n<a href=\"https://reactstarter.com/getting-started/\"><img src=\"https://img.shields.io/badge/Docs-007ec6\" height=\"20\"></a>\n<a href=\"https://github.com/kriasoft/react-starter-kit?sponsor=1\"><img src=\"https://img.shields.io/badge/-GitHub-%23555.svg?logo=github-sponsors\" height=\"20\"></a>\n<a href=\"https://discord.gg/2nKEnKq\"><img src=\"https://img.shields.io/discord/643523529131950086?label=Chat\" height=\"20\"></a>\n<a href=\"https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant\"><img src=\"https://img.shields.io/badge/Ask_ChatGPT-10a37f?logo=openai&logoColor=white\" height=\"20\"></a>\n<a href=\"https://gemini.google.com/gem/1IXFElQ2UvvZY86iL6uZLeoC-r8mp-OB-?usp=sharing\"><img src=\"https://img.shields.io/badge/Ask_Gemini-8E75B2?logo=googlegemini&logoColor=white\" height=\"20\"></a>\n<a href=\"https://github.com/kriasoft/react-starter-kit/stargazers\"><img src=\"https://img.shields.io/github/stars/kriasoft/react-starter-kit.svg?style=social&label=Star&maxAge=3600\" height=\"20\"></a>\n<a href=\"https://x.com/ReactStarter\"><img src=\"https://img.shields.io/twitter/follow/ReactStarter.svg?style=social&label=Follow&maxAge=3600\" height=\"20\"></a>\n\n</div>\n\nA full-stack monorepo template for building SaaS applications with React 19, tRPC, and Cloudflare Workers. Type-safe from database to UI, deployable to the edge in minutes.\n\n## Highlights\n\n- **Type-safe full stack** — TypeScript, tRPC, and Drizzle ORM create a single type contract from database to UI\n- **Edge-native** — Three Cloudflare Workers (web, app, api) connected via service bindings\n- **Auth + billing included** — Better Auth with email OTP, passkey, Google OAuth, organizations, and Stripe subscriptions\n- **Modern React** — React 19, TanStack Router (file-based), TanStack Query, Jotai, Tailwind CSS v4, shadcn/ui\n- **Database ready** — Drizzle ORM with Neon PostgreSQL, migrations, and seed data\n- **Fast DX** — Bun runtime, Vite, Vitest, ESLint, Prettier, and pre-configured VS Code settings\n\nReact Starter Kit is proudly supported by these amazing sponsors:\n\n<a href=\"https://reactstarter.com/s/1\"><img src=\"https://reactstarter.com/s/1.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/s/2\"><img src=\"https://reactstarter.com/s/2.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/s/3\"><img src=\"https://reactstarter.com/s/3.png\" height=\"60\" /></a>\n\n## Technology Stack\n\n| Layer         | Technologies                                                                                                                                                                                  |\n| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Runtime**   | [Bun](https://bun.sh/), [Cloudflare Workers](https://workers.cloudflare.com/), [TypeScript](https://www.typescriptlang.org/) 5.9                                                              |\n| **Frontend**  | [React 19](https://react.dev/), [TanStack Router](https://tanstack.com/router), [Tailwind CSS v4](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/), [Jotai](https://jotai.org/) |\n| **Marketing** | [Astro](https://astro.build/)                                                                                                                                                                 |\n| **Backend**   | [Hono](https://hono.dev/), [tRPC](https://trpc.io/), [Better Auth](https://www.better-auth.com/), [Stripe](https://stripe.com/)                                                               |\n| **Database**  | [Drizzle ORM](https://orm.drizzle.team/), [Neon PostgreSQL](https://get.neon.com/HD157BR)                                                                                                     |\n| **Tooling**   | [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), [ESLint](https://eslint.org/), [Prettier](https://prettier.io/)                                                                   |\n\n## Monorepo Architecture\n\n```\n├── apps/\n│   ├── web/          Astro marketing site (edge router, serves static + proxies to app/api)\n│   ├── app/          React 19 SPA (TanStack Router, Jotai, Tailwind CSS v4)\n│   ├── api/          Hono + tRPC API server (Better Auth, Cloudflare Workers)\n│   └── email/        React Email templates\n├── packages/\n│   ├── ui/           shadcn/ui components (new-york style)\n│   ├── core/         Shared types and utilities\n│   └── ws-protocol/  WebSocket protocol with type-safe messaging\n├── db/               Drizzle ORM schemas, migrations, and seed data\n├── infra/            Terraform (Cloudflare Workers, DNS, Hyperdrive)\n├── docs/             VitePress documentation\n└── scripts/          Build automation and dev tools\n```\n\nEach app deploys independently to Cloudflare Workers. The web worker routes `/api/*` to the API worker and app routes to the app worker via service bindings.\n\n## Prerequisites\n\n- [Bun](https://bun.sh/) v1.3+ (replaces Node.js and npm)\n- [VS Code](https://code.visualstudio.com/) with our [recommended extensions](.vscode/extensions.json)\n- [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en) browser extension (recommended)\n- [Cloudflare account](https://dash.cloudflare.com/sign-up) for deployment\n\n## Quick Start\n\n### 1. Create Your Project\n\n[Generate a new repository](https://github.com/kriasoft/react-starter-kit/generate) from this template, then clone it locally:\n\n```bash\ngit clone https://github.com/your-username/your-project-name.git\ncd your-project-name\n```\n\n### 2. Install Dependencies\n\n```bash\nbun install\n```\n\n### 3. Configure Environment\n\nThis project follows [Vite env conventions](https://vite.dev/guide/env-and-mode#env-files):\n\n- [`.env`](./.env) is committed and contains shared defaults/placeholders only (no real secrets)\n- `.env.local` is git-ignored and should contain your real credentials\n- `.env.local` values override `.env`\n\n```bash\ncp .env .env.local  # then replace placeholder values with real ones\n```\n\nAlso check [`wrangler.jsonc`](./apps/api/wrangler.jsonc) for Worker configuration and bindings.\n\n### 4. Start Development\n\n```bash\n# Launch all apps in development mode (web, api, and app)\nbun dev\n\n# Or, start specific apps individually\nbun web:dev  # Marketing site\nbun app:dev  # Main application\nbun api:dev  # API server\n```\n\n### 5. Initialize Database\n\nEnsure `DATABASE_URL` is configured in your `.env.local` file, then set up the schema:\n\n```bash\nbun db:push              # Push schema directly (quick dev setup)\nbun db:seed              # Seed with sample data (optional)\nbun db:studio            # Open database GUI\n```\n\nFor production, use `bun db:migrate` to apply migrations instead of `db:push`.\n\n| App            | URL                     |\n| -------------- | ----------------------- |\n| React app      | <http://localhost:5173> |\n| Marketing site | <http://localhost:4321> |\n| API server     | <http://localhost:8787> |\n\n## Production Deployment\n\n### 1. Environment Setup\n\nConfigure your production secrets in Cloudflare Workers:\n\n```bash\n# Required secrets\nbun wrangler secret put BETTER_AUTH_SECRET\n\n# Stripe billing (optional — first 4 required to enable, annual is optional)\nbun wrangler secret put STRIPE_SECRET_KEY\nbun wrangler secret put STRIPE_WEBHOOK_SECRET\nbun wrangler secret put STRIPE_STARTER_PRICE_ID\nbun wrangler secret put STRIPE_PRO_PRICE_ID\nbun wrangler secret put STRIPE_PRO_ANNUAL_PRICE_ID  # optional\n\n# OAuth providers (as needed)\nbun wrangler secret put GOOGLE_CLIENT_ID\nbun wrangler secret put GOOGLE_CLIENT_SECRET\n\n# Email service\nbun wrangler secret put RESEND_API_KEY\n\n# AI features (optional)\nbun wrangler secret put OPENAI_API_KEY\n```\n\nRun these commands from the target app directory or pass `--config apps/<app>/wrangler.jsonc`. Non-sensitive vars like `RESEND_EMAIL_FROM` go in `wrangler.jsonc` directly.\n\n### 2. Build and Deploy\n\n```bash\n# Build packages that require compilation (order matters!)\nbun email:build    # Build email templates first\nbun web:build      # Build marketing site\nbun app:build      # Build main React app\n\n# Deploy all applications\nbun web:deploy     # Deploy marketing site\nbun api:deploy     # Deploy API server\nbun app:deploy     # Deploy main React app\n```\n\n## Backers\n\n<a href=\"https://reactstarter.com/b/1\"><img src=\"https://reactstarter.com/b/1.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/2\"><img src=\"https://reactstarter.com/b/2.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/3\"><img src=\"https://reactstarter.com/b/3.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/4\"><img src=\"https://reactstarter.com/b/4.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/5\"><img src=\"https://reactstarter.com/b/5.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/6\"><img src=\"https://reactstarter.com/b/6.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/7\"><img src=\"https://reactstarter.com/b/7.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/b/8\"><img src=\"https://reactstarter.com/b/8.png\" height=\"60\" /></a>\n\n## Contributors\n\n<a href=\"https://reactstarter.com/c/1\"><img src=\"https://reactstarter.com/c/1.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/2\"><img src=\"https://reactstarter.com/c/2.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/3\"><img src=\"https://reactstarter.com/c/3.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/4\"><img src=\"https://reactstarter.com/c/4.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/5\"><img src=\"https://reactstarter.com/c/5.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/6\"><img src=\"https://reactstarter.com/c/6.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/7\"><img src=\"https://reactstarter.com/c/7.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/8\"><img src=\"https://reactstarter.com/c/8.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/9\"><img src=\"https://reactstarter.com/c/9.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/10\"><img src=\"https://reactstarter.com/c/10.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/11\"><img src=\"https://reactstarter.com/c/11.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/12\"><img src=\"https://reactstarter.com/c/12.png\" height=\"60\" /></a>&nbsp;&nbsp;<a href=\"https://reactstarter.com/c/13\"><img src=\"https://reactstarter.com/c/13.png\" height=\"60\" /></a>\n\n## Need Help?\n\n**[Documentation](https://reactstarter.com/)** covers auth, database, billing, deployment, and more.\n\nOur AI assistant is trained on this codebase — ask it anything on [ChatGPT](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant) or [Gemini](https://gemini.google.com/gem/1IXFElQ2UvvZY86iL6uZLeoC-r8mp-OB-?usp=sharing). Try these questions:\n\n- [How do I add a new tRPC endpoint?](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=How%20do%20I%20add%20a%20new%20tRPC%20endpoint%3F)\n- [Help me create a database table](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=Help%20me%20create%20a%20database%20table)\n- [How does authentication work?](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=How%20does%20authentication%20work%3F)\n- [Explain the project structure](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=Explain%20the%20project%20structure)\n- [How do I deploy to Cloudflare?](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=How%20do%20I%20deploy%20to%20Cloudflare%3F)\n- [Add a new page with routing](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=Add%20a%20new%20page%20with%20routing)\n- [How do I send emails?](https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant?prompt=How%20do%20I%20send%20emails%3F)\n\n## Contributing\n\nSee the [Contributing Guide](.github/CONTRIBUTING.md) to get started. Check [good first issues](https://github.com/kriasoft/react-starter-kit/issues?q=label:\"good+first+issue\") or join [Discord](https://discord.gg/2nKEnKq) for discussion.\n\n## License\n\nCopyright © 2014-present Kriasoft. This source code is licensed under the MIT license found in the\n[LICENSE](https://github.com/kriasoft/react-starter-kit/blob/main/LICENSE) file.\n\n---\n\n<sup>Made with ♥ by Konstantin Tarkus ([@koistya](https://twitter.com/koistya), [blog](https://medium.com/@koistya))\nand [contributors](https://github.com/kriasoft/react-starter-kit/graphs/contributors).</sup>\n"
  },
  {
    "path": "apps/api/AGENTS.md",
    "content": "## Auth\n\n- Server config in `lib/auth.ts`. Better Auth `account` table renamed to `identity` via `account.modelName: \"identity\"`.\n- Auth hint cookie: set/cleared in Better Auth hooks (sign-in, sign-out). NOT a security boundary — false positives cause one redirect. See `docs/adr/001-auth-hint-cookie.md`.\n- Session types: `AuthUser` and `AuthSession` from `Auth[\"$Infer\"][\"Session\"]`.\n\n## Database\n\n- Two Hyperdrive connections: `db` (cached, for reads) and `dbDirect` (no cache, for writes and transactions).\n- `prepare: false` required for Cloudflare Workers — avoids statement caching issues with connection pooling.\n- `max: 1` connection per instance (Workers cold-start model).\n- `transform: { undefined: null }` converts JS `undefined` to SQL `NULL`.\n\n## tRPC\n\n- `publicProcedure` and `protectedProcedure` defined in `lib/trpc.ts`.\n- `protectedProcedure` throws `UNAUTHORIZED` if `ctx.session` or `ctx.user` is null, then narrows both to non-null in downstream context.\n- Router in `lib/app.ts` combines routers from `routers/`. Input validation with Zod.\n\n## Email\n\n- Fresh `Resend` client per invocation via `createResendClient()`.\n- Requires both HTML and plain text — use `renderEmailToHtml()` + `renderEmailToText()` from `@repo/email`.\n- Validates recipients with Zod before sending.\n\n## Request Context\n\n- `ctx.cache: Map<string | symbol, unknown>` — request-scoped cache.\n- DataLoaders use `defineLoader(symbol, batchFn)` helper — handles cache check, instance creation, and typing. See `lib/loaders.ts`.\n- AI provider instances (OpenAI) also cached per-request via same pattern.\n\n## Environment\n\n- Zod schema validates env vars in `lib/env.ts` (Bun reads `Bun.env`; Workers get bindings via Hono context).\n- `nodejs_compat` compatibility flag required — web and app workers do NOT have it.\n\n## Worker Entry\n\n- `worker.ts` is the Cloudflare Workers entrypoint (`export default`). Hono middleware stack: `secureHeaders` → `requestId` (CF-Ray or UUID) → `logger` → context init (Drizzle + auth instances).\n"
  },
  {
    "path": "apps/api/Dockerfile",
    "content": "# Dockerfile for the tRPC API Server\n# docker build --tag api:latest -f ./apps/api/Dockerfile .\n# https://bun.com/guides/ecosystem/docker\n# https://docs.docker.com/guides/bun/containerize/\n\nFROM oven/bun:slim\n\n# Install dumb-init and jq for proper signal handling and JSON processing\nRUN apt-get update && apt-get install -y --no-install-recommends dumb-init jq && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Set environment variables\nENV NODE_ENV=production\n\n# Set working directory\nWORKDIR /usr/src/app\n\n# Copy package files for better layer caching\nCOPY ./apps/api/package.json ./package.json\n\n# Remove workspace dependencies from package.json\n# Workspace dependencies like \"workspace:*\" or \"workspace:^1.0.0\" cannot be resolved\n# inside Docker since the workspace packages aren't available in the container context.\n# This jq command filters out any dependency where the version starts with \"workspace:\"\n# from both dependencies and devDependencies sections\nRUN jq '.dependencies |= with_entries(select(.value | startswith(\"workspace:\") | not)) | \\\n    .devDependencies |= with_entries(select(.value | startswith(\"workspace:\") | not))' \\\n    package.json > package.tmp.json && \\\n    mv package.tmp.json package.json\n\n# Install production dependencies only\n# Using --production flag to exclude devDependencies\nRUN bun install --production\n\n# Copy pre-built server files from dist directory\n# The build process should be done outside of Docker\n# Note: Run `bun --filter @repo/api build` before building the Docker image\n# This bundles all dependencies including workspace packages into dist/index.js\nCOPY --chown=bun:bun ./apps/api/dist ./dist\n\n# Verify dist directory exists and has content\nRUN test -f ./dist/index.js || (echo \"Error: dist/index.js not found\" && exit 1)\n\n# Switch to non-root user\nUSER bun\n\n# Expose the port your server runs on (default: 8080)\nEXPOSE 8080\n\n# Run the server using dumb-init for proper signal handling\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\"]\nCMD [\"bun\", \"dist/index.js\"]\n"
  },
  {
    "path": "apps/api/README.md",
    "content": "# API Server\n\nHono + tRPC API server with Better Auth, Drizzle ORM, and Stripe billing. Runs on Cloudflare Workers.\n\n[Documentation](https://reactstarter.com/api/) | [Auth](https://reactstarter.com/auth/) | [Database](https://reactstarter.com/database/)\n\n## Development\n\n```bash\nbun api:dev       # Start dev server (http://localhost:8787)\nbun api:build     # Build for production\nbun api:deploy    # Deploy to Cloudflare Workers\n```\n\n## Structure\n\n```bash\nrouters/          # tRPC routers organized by domain\nlib/              # Context, middleware, DataLoaders, auth config\nworker.ts         # Cloudflare Worker entry point\nindex.ts          # Package exports\n```\n"
  },
  {
    "path": "apps/api/dev.ts",
    "content": "/**\n * @file Local development server emulating Cloudflare Workers runtime.\n *\n * Requires wrangler.jsonc with HYPERDRIVE_CACHED and HYPERDRIVE_DIRECT bindings.\n */\n\nimport { Hono } from \"hono\";\nimport { logger } from \"hono/logger\";\nimport { requestId } from \"hono/request-id\";\nimport { secureHeaders } from \"hono/secure-headers\";\nimport { parseArgs } from \"node:util\";\nimport { getPlatformProxy } from \"wrangler\";\nimport api from \"./index.js\";\nimport { createAuth } from \"./lib/auth.js\";\nimport type { AppContext } from \"./lib/context.js\";\nimport { createDb } from \"./lib/db.js\";\nimport type { Env } from \"./lib/env.js\";\nimport { errorHandler, notFoundHandler } from \"./lib/middleware.js\";\n\nconst { values: args } = parseArgs({\n  args: Bun.argv.slice(2),\n  options: {\n    env: { type: \"string\" },\n  },\n});\n\ntype CloudflareEnv = {\n  HYPERDRIVE_CACHED: Hyperdrive;\n  HYPERDRIVE_DIRECT: Hyperdrive;\n} & Env;\n\nconst app = new Hono<AppContext>();\n\n// Error and 404 handlers (must be on top-level app)\napp.onError(errorHandler);\napp.notFound(notFoundHandler);\n\n// Standard middleware\napp.use(secureHeaders());\napp.use(requestId());\napp.use(logger());\n\n// persist:true maintains state across restarts in .wrangler directory\nconst cf = await getPlatformProxy<CloudflareEnv>({\n  configPath: \"./wrangler.jsonc\",\n  environment: args.env ?? \"dev\",\n  persist: true,\n});\n\n// Inject context with two database connections:\n// - db: Hyperdrive caching for read-heavy queries\n// - dbDirect: No cache for writes and transactions\napp.use(async (c, next) => {\n  const db = createDb(cf.env.HYPERDRIVE_CACHED);\n  const dbDirect = createDb(cf.env.HYPERDRIVE_DIRECT);\n\n  // Merge secrets from process.env (local dev) with Cloudflare bindings\n  const secretKeys = [\n    \"BETTER_AUTH_SECRET\",\n    \"GOOGLE_CLIENT_ID\",\n    \"GOOGLE_CLIENT_SECRET\",\n    \"OPENAI_API_KEY\",\n    \"RESEND_API_KEY\",\n    \"RESEND_EMAIL_FROM\",\n    \"STRIPE_SECRET_KEY\",\n    \"STRIPE_WEBHOOK_SECRET\",\n    \"STRIPE_STARTER_PRICE_ID\",\n    \"STRIPE_PRO_PRICE_ID\",\n    \"STRIPE_PRO_ANNUAL_PRICE_ID\",\n  ] as const;\n\n  const env = {\n    ...cf.env,\n    ...Object.fromEntries(\n      secretKeys.map((key) => [key, process.env[key] || cf.env[key]]),\n    ),\n    APP_NAME: process.env.APP_NAME || cf.env.APP_NAME || \"Example\",\n    APP_ORIGIN:\n      c.req.header(\"x-forwarded-origin\") ||\n      process.env.APP_ORIGIN ||\n      c.env.APP_ORIGIN ||\n      \"http://localhost:5173\",\n  };\n\n  c.set(\"db\", db);\n  c.set(\"dbDirect\", dbDirect);\n  c.set(\"auth\", createAuth(db, env));\n  await next();\n});\n\napp.route(\"/\", api);\n\nexport default app;\n"
  },
  {
    "path": "apps/api/index.ts",
    "content": "/**\n * @file Public API surface for the backend package.\n *\n * Re-exports the Hono app, tRPC router, and core utilities.\n */\n\n// Core utilities and services\nexport { getOpenAI } from \"./lib/ai.js\";\nexport { createAuth } from \"./lib/auth.js\";\nexport { createDb } from \"./lib/db.js\";\n\n// Application and router exports\nexport { default as app, appRouter } from \"./lib/app.js\";\n\n// Type exports\nexport type { AppRouter } from \"./lib/app.js\";\nexport type { AppContext } from \"./lib/context.js\";\n// Re-export context type to fix TypeScript portability issues\nexport type * from \"./lib/context.js\";\n\n// Default export is the core app\nexport { default } from \"./lib/app.js\";\n"
  },
  {
    "path": "apps/api/lib/ai.ts",
    "content": "import type { OpenAIProvider } from \"@ai-sdk/openai\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport type { TRPCContext } from \"./context\";\n\ntype OpenAIContext = Pick<TRPCContext, \"env\" | \"cache\">;\n\n// Request-scoped cache key for the provider instance.\nconst OPENAI_PROVIDER = Symbol(\"openaiProvider\");\n\n/**\n * Returns a request-scoped OpenAI provider instance.\n * Pass the tRPC context to reuse the provider within a single request.\n */\nexport function getOpenAI(ctx: OpenAIContext): OpenAIProvider {\n  if (ctx.cache.has(OPENAI_PROVIDER)) {\n    return ctx.cache.get(OPENAI_PROVIDER) as OpenAIProvider;\n  }\n\n  const provider = createOpenAI({\n    apiKey: ctx.env.OPENAI_API_KEY,\n  });\n\n  ctx.cache.set(OPENAI_PROVIDER, provider);\n\n  return provider;\n}\n"
  },
  {
    "path": "apps/api/lib/app.ts",
    "content": "/**\n * @file Hono app construction and tRPC router initialization.\n *\n * Combines authentication, tRPC, and health check endpoints into a single HTTP router.\n */\n\nimport { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport { Hono } from \"hono\";\nimport type { AppContext } from \"./context.js\";\nimport { router } from \"./trpc.js\";\nimport { billingRouter } from \"../routers/billing.js\";\nimport { organizationRouter } from \"../routers/organization.js\";\nimport { userRouter } from \"../routers/user.js\";\n\n// tRPC API router\nconst appRouter = router({\n  billing: billingRouter,\n  user: userRouter,\n  organization: organizationRouter,\n});\n\n// HTTP router\nconst app = new Hono<AppContext>();\n\napp.get(\"/\", (c) => c.redirect(\"/api\"));\n\n// Root endpoint with API information\napp.get(\"/api\", (c) => {\n  return c.json({\n    name: \"@repo/api\",\n    version: \"0.0.0\",\n    endpoints: {\n      trpc: \"/api/trpc\",\n      auth: \"/api/auth\",\n      health: \"/health\",\n    },\n    documentation: {\n      trpc: \"https://trpc.io\",\n      auth: \"https://www.better-auth.com\",\n    },\n  });\n});\n\n// Health check endpoint\napp.get(\"/health\", (c) => {\n  return c.json({ status: \"healthy\", timestamp: new Date().toISOString() });\n});\n\n// Authentication routes\napp.on([\"GET\", \"POST\"], \"/api/auth/*\", (c) => {\n  const auth = c.get(\"auth\");\n  if (!auth) {\n    return c.json({ error: \"Authentication service not initialized\" }, 503);\n  }\n  return auth.handler(c.req.raw);\n});\n\n// tRPC API routes\napp.use(\"/api/trpc/*\", (c) => {\n  return fetchRequestHandler({\n    req: c.req.raw,\n    router: appRouter,\n    endpoint: \"/api/trpc\",\n    async createContext({ req, resHeaders, info }) {\n      const db = c.get(\"db\");\n      const dbDirect = c.get(\"dbDirect\");\n      const auth = c.get(\"auth\");\n\n      if (!db) {\n        throw new Error(\"Database not available in context\");\n      }\n\n      if (!dbDirect) {\n        throw new Error(\"Direct database not available in context\");\n      }\n\n      if (!auth) {\n        throw new Error(\"Authentication service not available in context\");\n      }\n\n      const sessionData = await auth.api.getSession({\n        headers: req.headers,\n      });\n\n      return {\n        req,\n        res: c.res,\n        resHeaders,\n        info,\n        env: c.env,\n        db,\n        dbDirect,\n        session: sessionData?.session ?? null,\n        user: sessionData?.user ?? null,\n        cache: new Map(),\n      };\n    },\n    batching: {\n      enabled: true,\n    },\n    onError({ error, path }) {\n      console.error(\"tRPC error on path\", path, \":\", error);\n    },\n  });\n});\n\nexport { appRouter };\nexport type AppRouter = typeof appRouter;\nexport default app;\n"
  },
  {
    "path": "apps/api/lib/auth.ts",
    "content": "import { passkey } from \"@better-auth/passkey\";\nimport { stripe } from \"@better-auth/stripe\";\nimport { schema as Db, generateAuthId, type AuthModel } from \"@repo/db\";\nimport { betterAuth } from \"better-auth\";\nimport type { DB } from \"better-auth/adapters/drizzle\";\nimport { drizzleAdapter } from \"better-auth/adapters/drizzle\";\nimport { createAuthMiddleware } from \"better-auth/api\";\nimport { anonymous, organization } from \"better-auth/plugins\";\nimport { emailOTP } from \"better-auth/plugins/email-otp\";\nimport { and, eq } from \"drizzle-orm\";\nimport { sendOTP, sendPasswordReset, sendVerificationEmail } from \"./email\";\nimport type { Env } from \"./env\";\nimport { planLimits } from \"./plans\";\nimport { createStripeClient } from \"./stripe\";\n\n// Auth hint cookie for edge routing (see docs/adr/001-auth-hint-cookie.md)\n// NOT a security boundary - false positives are acceptable (causes one redirect)\n// __Host- prefix requires Secure; use plain name in HTTP dev\nconst AUTH_HINT_VALUE = \"1\";\n\n/**\n * Environment variables required for authentication configuration.\n * Extracted from the main Env type for better type safety and documentation.\n */\ntype AuthEnv = Pick<\n  Env,\n  | \"ENVIRONMENT\"\n  | \"APP_NAME\"\n  | \"APP_ORIGIN\"\n  | \"BETTER_AUTH_SECRET\"\n  | \"GOOGLE_CLIENT_ID\"\n  | \"GOOGLE_CLIENT_SECRET\"\n  | \"RESEND_API_KEY\"\n  | \"RESEND_EMAIL_FROM\"\n  | \"STRIPE_SECRET_KEY\"\n  | \"STRIPE_WEBHOOK_SECRET\"\n  | \"STRIPE_STARTER_PRICE_ID\"\n  | \"STRIPE_PRO_PRICE_ID\"\n  | \"STRIPE_PRO_ANNUAL_PRICE_ID\"\n>;\n\n/**\n * Stripe billing plugin — only enabled when all required env vars are set.\n * Without Stripe config, the app works but billing endpoints return 404.\n */\nfunction stripePlugin(db: DB, env: AuthEnv) {\n  if (\n    !env.STRIPE_SECRET_KEY ||\n    !env.STRIPE_WEBHOOK_SECRET ||\n    !env.STRIPE_STARTER_PRICE_ID ||\n    !env.STRIPE_PRO_PRICE_ID\n  ) {\n    return [];\n  }\n\n  return [\n    stripe({\n      stripeClient: createStripeClient(env),\n      stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,\n      createCustomerOnSignUp: true,\n      subscription: {\n        enabled: true,\n        plans: [\n          {\n            name: \"starter\",\n            priceId: env.STRIPE_STARTER_PRICE_ID,\n            limits: planLimits.starter,\n          },\n          {\n            name: \"pro\",\n            priceId: env.STRIPE_PRO_PRICE_ID,\n            annualDiscountPriceId: env.STRIPE_PRO_ANNUAL_PRICE_ID,\n            limits: planLimits.pro,\n            freeTrial: { days: 14 },\n          },\n        ],\n        // Personal billing: user can manage their own subscription.\n        // Organization billing: only owner/admin can manage.\n        authorizeReference: async ({ user, referenceId }) => {\n          if (referenceId === user.id) return true;\n          const [row] = await db\n            .select({ role: Db.member.role })\n            .from(Db.member)\n            .where(\n              and(\n                eq(Db.member.organizationId, referenceId),\n                eq(Db.member.userId, user.id),\n              ),\n            );\n          return row?.role === \"owner\" || row?.role === \"admin\";\n        },\n      },\n      organization: { enabled: true },\n    }),\n  ];\n}\n\n/**\n * Creates a Better Auth instance configured for multi-tenant SaaS with organization support.\n *\n * Key behaviors:\n * - Uses custom 'identity' table instead of default 'account' model for OAuth accounts\n * - Allows users to create up to 5 organizations with 'owner' role as creator\n * - Generates prefixed CUID2 IDs at application level (e.g. usr_..., ses_...)\n * - Supports anonymous authentication alongside email/password and Google OAuth\n *\n * @param db Drizzle database instance - must include all required auth tables (user, session, identity, organization, member, invitation, verification)\n * @param env Environment variables containing auth secrets and OAuth credentials\n * @returns Configured Better Auth instance with email/password and Google OAuth\n * @remarks Missing database tables will cause runtime errors when auth endpoints are called.\n *\n * @example\n * ```ts\n * const auth = createAuth(database, {\n *   BETTER_AUTH_SECRET: \"your-secret\",\n *   GOOGLE_CLIENT_ID: \"google-id\",\n *   GOOGLE_CLIENT_SECRET: \"google-secret\"\n * });\n * ```\n */\nexport function createAuth(\n  db: DB,\n  env: AuthEnv,\n): ReturnType<typeof betterAuth> {\n  // Extract domain from APP_ORIGIN for passkey rpID\n  const appUrl = new URL(env.APP_ORIGIN);\n  const rpID = appUrl.hostname;\n\n  return betterAuth({\n    baseURL: `${env.APP_ORIGIN}/api/auth`,\n    trustedOrigins: [env.APP_ORIGIN],\n    secret: env.BETTER_AUTH_SECRET,\n    database: drizzleAdapter(db, {\n      provider: \"pg\",\n\n      schema: {\n        identity: Db.identity,\n        invitation: Db.invitation,\n        member: Db.member,\n        organization: Db.organization,\n        passkey: Db.passkey,\n        session: Db.session,\n        subscription: Db.subscription,\n        user: Db.user,\n        verification: Db.verification,\n      },\n    }),\n\n    account: {\n      modelName: \"identity\",\n    },\n\n    // Email and password authentication\n    emailAndPassword: {\n      enabled: true,\n      sendResetPassword: async ({ user, url }) => {\n        await sendPasswordReset(env, { user, url });\n      },\n    },\n\n    // Email verification\n    emailVerification: {\n      sendVerificationEmail: async ({ user, url }) => {\n        await sendVerificationEmail(env, { user, url });\n      },\n    },\n\n    // OAuth providers\n    socialProviders: {\n      google: {\n        clientId: env.GOOGLE_CLIENT_ID,\n        clientSecret: env.GOOGLE_CLIENT_SECRET,\n      },\n    },\n\n    plugins: [\n      anonymous(),\n      organization({\n        allowUserToCreateOrganization: true,\n        organizationLimit: 5,\n        creatorRole: \"owner\",\n      }),\n      passkey({\n        // rpID: Relying Party ID - domain name in production, 'localhost' for dev\n        rpID,\n        // rpName: Human-readable name for your app\n        rpName: env.APP_NAME,\n        // origin: URL where auth occurs (no trailing slash)\n        origin: env.APP_ORIGIN,\n      }),\n      emailOTP({\n        async sendVerificationOTP({ email, otp, type }) {\n          await sendOTP(env, { email, otp, type });\n        },\n        otpLength: 6,\n        expiresIn: 300, // 5 minutes\n        allowedAttempts: 3,\n      }),\n      ...stripePlugin(db, env),\n    ],\n\n    advanced: {\n      database: {\n        generateId: ({ model }) => generateAuthId(model as AuthModel),\n      },\n    },\n\n    // Set/clear auth hint cookie for edge routing\n    hooks: {\n      after: createAuthMiddleware(async (ctx) => {\n        const isSecure = new URL(env.APP_ORIGIN).protocol === \"https:\";\n        // __Host- prefix requires Secure; browsers reject it over HTTP\n        const cookieName = isSecure ? \"__Host-auth\" : \"auth\";\n        const cookieOpts = {\n          path: \"/\",\n          secure: isSecure,\n          httpOnly: true,\n          sameSite: \"lax\" as const,\n        };\n\n        // Set hint cookie on session creation (sign-in, sign-up, OAuth callback)\n        if (ctx.context.newSession) {\n          ctx.setCookie(cookieName, AUTH_HINT_VALUE, cookieOpts);\n          return;\n        }\n\n        // Clear hint cookie on sign-out\n        // ctx.path is normalized (base path stripped) by better-call router\n        if (ctx.path.startsWith(\"/sign-out\")) {\n          ctx.setCookie(cookieName, \"\", { ...cookieOpts, maxAge: 0 });\n          return;\n        }\n\n        // Clear stale hint cookie on session check when session is invalid\n        // Only run on /get-session where ctx.context.session is reliably populated\n        // This handles: expired sessions, revoked sessions, deleted users\n        if (ctx.path === \"/get-session\" && !ctx.context.session) {\n          const cookies = ctx.request?.headers.get(\"cookie\") ?? \"\";\n          const hasHintCookie = cookies\n            .split(\";\")\n            .some((c) => c.trim().startsWith(`${cookieName}=`));\n          if (hasHintCookie) {\n            ctx.setCookie(cookieName, \"\", { ...cookieOpts, maxAge: 0 });\n          }\n        }\n      }),\n    },\n  });\n}\n\nexport type Auth = ReturnType<typeof betterAuth>;\n\n// Base session types from Better Auth - plugin-specific fields added at runtime\ntype SessionResponse = Auth[\"$Infer\"][\"Session\"];\nexport type AuthUser = SessionResponse[\"user\"];\n// Organization plugin adds activeOrganizationId at runtime\nexport type AuthSession = SessionResponse[\"session\"] & {\n  activeOrganizationId?: string;\n};\n"
  },
  {
    "path": "apps/api/lib/context.ts",
    "content": "import type { DatabaseSchema } from \"@repo/db\";\nimport type { CreateHTTPContextOptions } from \"@trpc/server/adapters/standalone\";\nimport type { PostgresJsDatabase } from \"drizzle-orm/postgres-js\";\nimport type { Resend } from \"resend\";\nimport type { Auth, AuthSession, AuthUser } from \"./auth.js\";\nimport type { Env } from \"./env.js\";\n\n/**\n * Context object passed to all tRPC procedures.\n *\n * @remarks\n * This context is created for each incoming request and provides access to:\n * - Request-specific data (headers, session, etc.)\n * - Shared resources (database, cache)\n * - Environment configuration\n *\n * The context is immutable within a single request but can be extended\n * by middleware functions before reaching the procedure.\n *\n * @example\n * ```typescript\n * // Access context in a tRPC procedure\n * export const getUser = publicProcedure\n *   .input(z.object({ id: z.string() }))\n *   .query(async ({ ctx, input }) => {\n *     return await ctx.db.select().from(user).where(eq(user.id, input.id));\n *   });\n * ```\n */\nexport type TRPCContext = {\n  /** The incoming HTTP request object */\n  req: Request;\n\n  /** tRPC request metadata (headers, connection info) */\n  info: CreateHTTPContextOptions[\"info\"];\n\n  /** Drizzle ORM database instance (PostgreSQL via Hyperdrive cached connection) */\n  db: PostgresJsDatabase<DatabaseSchema>;\n\n  /** Drizzle ORM database instance (PostgreSQL via Hyperdrive direct connection) */\n  dbDirect: PostgresJsDatabase<DatabaseSchema>;\n\n  /** Authenticated user session (null if not authenticated) */\n  session: AuthSession | null;\n\n  /** Authenticated user data (null if not authenticated) */\n  user: AuthUser | null;\n\n  /** Request-scoped cache for storing computed values during request lifecycle */\n  cache: Map<string | symbol, unknown>;\n\n  /** Optional HTTP response object (available in Hono middleware) */\n  res?: Response;\n\n  /** Optional response headers (for setting cookies, CORS headers, etc.) */\n  resHeaders?: Headers;\n\n  /** Environment variables and secrets */\n  env: Env;\n};\n\n/**\n * Hono application context.\n *\n * @example\n * ```typescript\n * app.get(\"/api/health\", async (c) => {\n *   const db = c.get(\"db\");\n *   const user = c.get(\"user\");\n *   return c.json({ status: \"ok\", user: user?.email });\n * });\n * ```\n */\nexport type AppContext = {\n  Bindings: Env;\n  Variables: {\n    db: PostgresJsDatabase<DatabaseSchema>;\n    dbDirect: PostgresJsDatabase<DatabaseSchema>;\n    auth: Auth;\n    resend?: Resend;\n    session: AuthSession | null;\n    user: AuthUser | null;\n  };\n};\n"
  },
  {
    "path": "apps/api/lib/db.ts",
    "content": "/**\n * @file Database client using Neon PostgreSQL via Cloudflare Hyperdrive.\n *\n * Two bindings available: HYPERDRIVE_CACHED (60s cache) and HYPERDRIVE_DIRECT (no cache).\n */\n\nimport { schema } from \"@repo/db\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\n\n/**\n * Creates a database client using Drizzle ORM and Cloudflare Hyperdrive.\n *\n * @param db - Cloudflare Hyperdrive binding providing connection string\n */\nexport function createDb(db: Hyperdrive) {\n  const client = postgres(db.connectionString, {\n    max: 1,\n    connect_timeout: 10,\n    prepare: false, // Avoids prepared statement caching issues in Workers\n    idle_timeout: 20,\n    max_lifetime: 60 * 30,\n    transform: {\n      undefined: null,\n    },\n    onnotice: () => {},\n  });\n\n  return drizzle(client, { schema, casing: \"snake_case\" });\n}\n\nexport { schema as Db };\n"
  },
  {
    "path": "apps/api/lib/email.ts",
    "content": "import {\n  EmailVerification,\n  OTPEmail,\n  PasswordReset,\n  renderEmailToHtml,\n  renderEmailToText,\n} from \"@repo/email\";\nimport { Resend } from \"resend\";\nimport { z } from \"zod\";\nimport type { Env } from \"./env\";\n\nexport interface EmailOptions {\n  to: string | string[];\n  subject: string;\n  html?: string;\n  text?: string;\n  from?: string;\n}\n\nexport function createResendClient(apiKey: string): Resend {\n  if (!apiKey) {\n    throw new Error(\"RESEND_API_KEY is required\");\n  }\n  return new Resend(apiKey);\n}\n\n/**\n * Send an email using the Resend client.\n *\n * @param env Environment variables containing Resend configuration\n * @param options Email configuration\n */\nexport async function sendEmail(\n  env: Pick<Env, \"RESEND_API_KEY\" | \"RESEND_EMAIL_FROM\">,\n  options: EmailOptions,\n) {\n  const emailSchema = z.email();\n\n  // Validate all recipients before sending\n  const recipients = Array.isArray(options.to) ? options.to : [options.to];\n  for (const email of recipients) {\n    const result = emailSchema.safeParse(email);\n    if (!result.success) {\n      throw new Error(`Invalid email address: ${email}`);\n    }\n  }\n\n  if (!env.RESEND_EMAIL_FROM) {\n    throw new Error(\"RESEND_EMAIL_FROM environment variable is required\");\n  }\n\n  const resend = createResendClient(env.RESEND_API_KEY);\n\n  if (!options.text && !options.html) {\n    throw new Error(\"Either text or html content is required\");\n  }\n\n  if (options.html && !options.text) {\n    throw new Error(\n      \"Plain text version required when sending HTML email. Use renderEmailToText() from @repo/email.\",\n    );\n  }\n\n  try {\n    const result = await resend.emails.send({\n      from: options.from || env.RESEND_EMAIL_FROM,\n      to: options.to,\n      subject: options.subject,\n      html: options.html,\n      text: options.text as string,\n    });\n\n    if (result.error) {\n      throw new Error(\n        `Resend API error: ${result.error.message || result.error.name || \"Unknown error\"}`,\n      );\n    }\n\n    return result;\n  } catch (error) {\n    throw new Error(\n      `Failed to send email: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n      { cause: error },\n    );\n  }\n}\n\n/**\n * Send email verification message.\n *\n * @param env Environment variables\n * @param options User and verification URL (should be time-limited, signed token)\n */\nexport async function sendVerificationEmail(\n  env: Pick<\n    Env,\n    \"RESEND_API_KEY\" | \"RESEND_EMAIL_FROM\" | \"APP_NAME\" | \"APP_ORIGIN\"\n  >,\n  options: {\n    user: { email: string; name?: string };\n    url: string;\n  },\n) {\n  const component = EmailVerification({\n    userName: options.user.name,\n    verificationUrl: options.url,\n    appName: env.APP_NAME,\n    appUrl: env.APP_ORIGIN,\n  });\n\n  const html = await renderEmailToHtml(component);\n  const text = await renderEmailToText(component);\n\n  return sendEmail(env, {\n    to: options.user.email,\n    subject: \"Verify your email address\",\n    html,\n    text,\n  });\n}\n\n/**\n * Send password reset email.\n *\n * @param env Environment variables\n * @param options User and reset URL (must be single-use token with short expiration)\n */\nexport async function sendPasswordReset(\n  env: Pick<\n    Env,\n    \"RESEND_API_KEY\" | \"RESEND_EMAIL_FROM\" | \"APP_NAME\" | \"APP_ORIGIN\"\n  >,\n  options: {\n    user: { email: string; name?: string };\n    url: string;\n  },\n) {\n  const component = PasswordReset({\n    userName: options.user.name,\n    resetUrl: options.url,\n    appName: env.APP_NAME,\n    appUrl: env.APP_ORIGIN,\n  });\n\n  const html = await renderEmailToHtml(component);\n  const text = await renderEmailToText(component);\n\n  return sendEmail(env, {\n    to: options.user.email,\n    subject: \"Reset your password\",\n    html,\n    text,\n  });\n}\n\n/**\n * Send OTP email for authentication.\n *\n * @param env Environment variables\n * @param options Email, OTP code (must be rate-limited, time-bound, single-use), and type\n */\nexport async function sendOTP(\n  env: Pick<\n    Env,\n    | \"ENVIRONMENT\"\n    | \"RESEND_API_KEY\"\n    | \"RESEND_EMAIL_FROM\"\n    | \"APP_NAME\"\n    | \"APP_ORIGIN\"\n  >,\n  options: {\n    email: string;\n    otp: string;\n    type: \"sign-in\" | \"email-verification\" | \"forget-password\";\n  },\n) {\n  if (env.ENVIRONMENT === \"development\") {\n    console.log(`OTP code for ${options.email}: ${options.otp}`);\n  }\n\n  const component = OTPEmail({\n    otp: options.otp,\n    type: options.type,\n    appName: env.APP_NAME,\n    appUrl: env.APP_ORIGIN,\n  });\n\n  const html = await renderEmailToHtml(component);\n  const text = await renderEmailToText(component);\n\n  const typeLabels = {\n    \"sign-in\": \"Sign In\",\n    \"email-verification\": \"Email Verification\",\n    \"forget-password\": \"Password Reset\",\n  };\n\n  return sendEmail(env, {\n    to: options.email,\n    subject: `Your ${typeLabels[options.type]} code`,\n    html,\n    text,\n  });\n}\n"
  },
  {
    "path": "apps/api/lib/env.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Zod schema for validating environment variables.\n * Ensures all required configuration values are present and correctly formatted.\n *\n * @throws {ZodError} When environment variables don't match the schema\n */\nexport const envSchema = z.object({\n  ENVIRONMENT: z.enum([\"production\", \"staging\", \"preview\", \"development\"]),\n  APP_NAME: z.string().default(\"Example\"),\n  APP_ORIGIN: z.url(),\n  DATABASE_URL: z.url(),\n  BETTER_AUTH_SECRET: z.string().min(32),\n  GOOGLE_CLIENT_ID: z.string(),\n  GOOGLE_CLIENT_SECRET: z.string(),\n  OPENAI_API_KEY: z.string(),\n  RESEND_API_KEY: z.string(),\n  RESEND_EMAIL_FROM: z.email(),\n  // Stripe billing (optional — app works without these, billing features disabled)\n  STRIPE_SECRET_KEY: z.string().startsWith(\"sk_\").optional(),\n  STRIPE_WEBHOOK_SECRET: z.string().startsWith(\"whsec_\").optional(),\n  STRIPE_STARTER_PRICE_ID: z.string().startsWith(\"price_\").optional(),\n  STRIPE_PRO_PRICE_ID: z.string().startsWith(\"price_\").optional(),\n  STRIPE_PRO_ANNUAL_PRICE_ID: z.string().startsWith(\"price_\").optional(),\n});\n\n/**\n * Runtime environment variables accessor.\n *\n * @remarks\n * - In Bun runtime: Variables are accessed via `Bun.env`\n * - In Cloudflare Workers: Variables must be accessed via request context\n * - Falls back to empty object when Bun global is unavailable\n *\n * @example\n * // In Bun runtime\n * const dbUrl = env.DATABASE_URL;\n *\n * // In Cloudflare Workers (must use context)\n * const dbUrl = context.env.DATABASE_URL;\n */\nexport const env =\n  typeof Bun === \"undefined\" ? ({} as Env) : envSchema.parse(Bun.env);\n\n/**\n * Type-safe environment variables interface.\n * Inferred from the Zod schema to ensure type safety.\n */\nexport type Env = z.infer<typeof envSchema>;\n"
  },
  {
    "path": "apps/api/lib/loaders.ts",
    "content": "/**\n * Request-scoped DataLoaders for batching and N+1 prevention.\n *\n * @example\n * ```ts\n * protectedProcedure\n *   .query(async ({ ctx }) => {\n *     const user = await userById(ctx).load(ctx.session.userId);\n *   })\n * ```\n */\n\nimport DataLoader from \"dataloader\";\nimport { inArray } from \"drizzle-orm\";\nimport { user } from \"@repo/db/schema/user.js\";\nimport type { TRPCContext } from \"./context\";\n\n/** Map fetched items by key, preserving input order (nulls for missing). */\nfunction mapByKey<T, K extends keyof T>(\n  items: T[],\n  keyField: K,\n  keys: readonly T[K][],\n): (T | null)[] {\n  const map = new Map(items.map((item) => [item[keyField], item]));\n  return keys.map((k) => map.get(k) ?? null);\n}\n\n/** Create a request-scoped DataLoader (one instance per request via ctx.cache). */\nfunction defineLoader<K, V>(\n  key: symbol,\n  batchFn: (ctx: TRPCContext, keys: readonly K[]) => Promise<(V | null)[]>,\n): (ctx: TRPCContext) => DataLoader<K, V | null> {\n  return (ctx) => {\n    let loader = ctx.cache.get(key) as DataLoader<K, V | null> | undefined;\n    if (!loader) {\n      loader = new DataLoader((keys: readonly K[]) => batchFn(ctx, keys));\n      ctx.cache.set(key, loader);\n    }\n    return loader;\n  };\n}\n\nexport const userById = defineLoader(\n  Symbol(\"userById\"),\n  async (ctx, ids: readonly string[]) => {\n    const users = await ctx.db\n      .select()\n      .from(user)\n      .where(inArray(user.id, [...ids]));\n    return mapByKey(users, \"id\", ids);\n  },\n);\n\nexport const userByEmail = defineLoader(\n  Symbol(\"userByEmail\"),\n  async (ctx, emails: readonly string[]) => {\n    const users = await ctx.db\n      .select()\n      .from(user)\n      .where(inArray(user.email, [...emails]));\n    return mapByKey(users, \"email\", emails);\n  },\n);\n"
  },
  {
    "path": "apps/api/lib/middleware.ts",
    "content": "/**\n * @file Shared Hono middleware for both production and development entrypoints.\n */\n\nimport type { Context, ErrorHandler, NotFoundHandler } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\n\n/**\n * Global error handler for top-level Hono apps.\n *\n * Handles HTTPException specially (returns its response),\n * logs unexpected errors and returns a generic 500.\n */\nexport const errorHandler: ErrorHandler = (err, c) => {\n  if (err instanceof HTTPException) {\n    // getResponse() is not context-aware; merge headers from middleware\n    const res = err.getResponse();\n    const headers = new Headers(res.headers);\n    c.res.headers.forEach((v, k) => headers.set(k, v));\n    return new Response(res.body, {\n      status: res.status,\n      statusText: res.statusText,\n      headers,\n    });\n  }\n  console.error(`[${c.req.method}] ${c.req.path}:`, err);\n  return c.json({ error: \"Internal Server Error\" }, 500);\n};\n\n/**\n * 404 handler for unmatched routes.\n *\n * Must be registered on top-level app (notFound on mounted sub-apps is ignored).\n */\nexport const notFoundHandler: NotFoundHandler = (c) => {\n  return c.json({ error: \"Not Found\", path: c.req.path }, 404);\n};\n\n/**\n * Request ID generator using Cloudflare Ray ID when available.\n */\nexport function requestIdGenerator(c: Context): string {\n  return c.req.header(\"cf-ray\") ?? crypto.randomUUID();\n}\n"
  },
  {
    "path": "apps/api/lib/plans.ts",
    "content": "// Single source of truth for plan limits.\n// Referenced by auth plugin config (plan definitions) and tRPC router (query responses).\n\nexport const planLimits = {\n  free: { members: 1 },\n  starter: { members: 5 },\n  pro: { members: 50 },\n} as const;\n\nexport type PlanName = keyof typeof planLimits;\n"
  },
  {
    "path": "apps/api/lib/stripe.ts",
    "content": "import Stripe from \"stripe\";\nimport type { Env } from \"./env\";\n\n// Only called when STRIPE_SECRET_KEY is verified present (see auth.ts conditional)\nexport function createStripeClient(env: Pick<Env, \"STRIPE_SECRET_KEY\">) {\n  return new Stripe(env.STRIPE_SECRET_KEY!, {\n    appInfo: { name: \"React Starter Kit\" },\n  });\n}\n"
  },
  {
    "path": "apps/api/lib/trpc.ts",
    "content": "import { initTRPC, TRPCError, type TRPCProcedureBuilder } from \"@trpc/server\";\nimport { flattenError, ZodError } from \"zod\";\nimport type { TRPCContext } from \"./context.js\";\n\nconst t = initTRPC.context<TRPCContext>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError ? flattenError(error.cause) : null,\n      },\n    };\n  },\n});\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\nexport const createCallerFactory = t.createCallerFactory;\n\n// Derive type from publicProcedure to stay in sync with initTRPC config.\n// Explicit annotation required to avoid TS2742 (non-portable inferred type).\ntype ProtectedProcedure =\n  typeof publicProcedure extends TRPCProcedureBuilder<\n    infer TContext,\n    infer TMeta,\n    // eslint-disable-next-line @typescript-eslint/no-unused-vars\n    infer TContextOverrides,\n    infer TInputIn,\n    infer TInputOut,\n    infer TOutputIn,\n    infer TOutputOut,\n    infer TCaller\n  >\n    ? TRPCProcedureBuilder<\n        TContext,\n        TMeta,\n        {\n          session: NonNullable<TRPCContext[\"session\"]>;\n          user: NonNullable<TRPCContext[\"user\"]>;\n        },\n        TInputIn,\n        TInputOut,\n        TOutputIn,\n        TOutputOut,\n        TCaller\n      >\n    : never;\n\nexport const protectedProcedure: ProtectedProcedure = t.procedure.use(\n  ({ ctx, next }) => {\n    if (!ctx.session || !ctx.user) {\n      throw new TRPCError({\n        code: \"UNAUTHORIZED\",\n        message: \"Authentication required\",\n      });\n    }\n    return next({\n      ctx: {\n        ...ctx,\n        session: ctx.session,\n        user: ctx.user,\n      },\n    });\n  },\n);\n"
  },
  {
    "path": "apps/api/package.json",
    "content": "{\n  \"name\": \"@repo/api\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./index.ts\",\n    \"./auth\": \"./lib/auth.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"predev\": \"bun --filter @repo/email build\",\n    \"dev\": \"bun run --watch --env-file ../../.env --env-file ../../.env.local ./dev.ts\",\n    \"build\": \"bun build index.ts --outdir dist --target bun\",\n    \"test\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"deploy\": \"wrangler deploy --env-file ../../.env --env-file ../../.env.local\",\n    \"logs\": \"wrangler tail --env-file ../../.env --env-file ../../.env.local\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/openai\": \"^3.0.29\",\n    \"@better-auth/passkey\": \"^1.4.18\",\n    \"@better-auth/stripe\": \"^1.4.18\",\n    \"@repo/core\": \"workspace:*\",\n    \"@repo/db\": \"workspace:*\",\n    \"@repo/email\": \"workspace:*\",\n    \"@trpc/server\": \"^11.10.0\",\n    \"ai\": \"^6.0.91\",\n    \"better-auth\": \"^1.4.18\",\n    \"dataloader\": \"^2.2.3\",\n    \"drizzle-orm\": \"^0.45.1\",\n    \"postgres\": \"^3.4.8\",\n    \"resend\": \"^6.9.2\",\n    \"stripe\": \"^20.3.1\"\n  },\n  \"peerDependencies\": {\n    \"hono\": \"^4.11.10\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20260218.0\",\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/bun\": \"^1.3.9\",\n    \"hono\": \"^4.11.10\",\n    \"typescript\": \"~5.9.3\",\n    \"vitest\": \"~4.0.18\",\n    \"wrangler\": \"^4.66.0\",\n    \"zod\": \"^4.3.6\"\n  }\n}\n"
  },
  {
    "path": "apps/api/routers/billing.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\nimport type { TRPCContext } from \"../lib/context\";\nimport { createCallerFactory } from \"../lib/trpc\";\nimport { billingRouter } from \"./billing\";\n\nconst createCaller = createCallerFactory(billingRouter);\n\n// Minimal context mock — only fields the billing procedure accesses.\nfunction testCtx({\n  userId = \"user-1\",\n  activeOrgId = undefined as string | undefined,\n  subscription = undefined as Record<string, unknown> | undefined,\n} = {}) {\n  const ctx: TRPCContext = {\n    req: new Request(\"http://localhost\"),\n    info: {} as TRPCContext[\"info\"],\n    session: {\n      id: \"s-1\",\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      userId,\n      expiresAt: new Date(Date.now() + 60_000),\n      token: \"token\",\n      activeOrganizationId: activeOrgId,\n    },\n    user: {\n      id: userId,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      email: \"test@example.com\",\n      emailVerified: true,\n      name: \"Test User\",\n    },\n    db: {\n      query: {\n        subscription: {\n          findFirst: vi.fn().mockResolvedValue(subscription),\n        },\n      },\n    } as unknown as TRPCContext[\"db\"],\n    dbDirect: {} as TRPCContext[\"dbDirect\"],\n    cache: new Map(),\n    env: {} as TRPCContext[\"env\"],\n  };\n\n  return ctx;\n}\n\ndescribe(\"billing.subscription\", () => {\n  it(\"returns free plan defaults when no subscription exists\", async () => {\n    const result = await createCaller(testCtx()).subscription();\n\n    expect(result).toEqual({\n      plan: \"free\",\n      status: null,\n      periodEnd: null,\n      cancelAtPeriodEnd: false,\n      limits: { members: 1 },\n    });\n  });\n\n  it(\"returns active subscription with plan limits\", async () => {\n    const periodEnd = new Date(\"2025-03-01\");\n    const result = await createCaller(\n      testCtx({\n        subscription: {\n          plan: \"pro\",\n          status: \"active\",\n          periodEnd,\n          cancelAtPeriodEnd: false,\n        },\n      }),\n    ).subscription();\n\n    expect(result).toEqual({\n      plan: \"pro\",\n      status: \"active\",\n      periodEnd,\n      cancelAtPeriodEnd: false,\n      limits: { members: 50 },\n    });\n  });\n\n  it(\"returns trialing subscription\", async () => {\n    const result = await createCaller(\n      testCtx({\n        subscription: {\n          plan: \"starter\",\n          status: \"trialing\",\n          periodEnd: null,\n          cancelAtPeriodEnd: false,\n        },\n      }),\n    ).subscription();\n\n    expect(result.plan).toBe(\"starter\");\n    expect(result.status).toBe(\"trialing\");\n    expect(result.limits).toEqual({ members: 5 });\n  });\n\n  it(\"maps cancelAtPeriodEnd flag\", async () => {\n    const result = await createCaller(\n      testCtx({\n        subscription: {\n          plan: \"pro\",\n          status: \"active\",\n          periodEnd: new Date(),\n          cancelAtPeriodEnd: true,\n        },\n      }),\n    ).subscription();\n\n    expect(result.cancelAtPeriodEnd).toBe(true);\n  });\n\n  it(\"throws on unknown plan name\", async () => {\n    await expect(\n      createCaller(\n        testCtx({\n          subscription: { plan: \"enterprise\", status: \"active\" },\n        }),\n      ).subscription(),\n    ).rejects.toThrow('Unknown plan \"enterprise\"');\n  });\n});\n"
  },
  {
    "path": "apps/api/routers/billing.ts",
    "content": "import { planLimits, type PlanName } from \"../lib/plans.js\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const billingRouter = router({\n  // Active subscription + limits for the current billing reference.\n  // referenceId is derived from session — org billing when an org is active,\n  // personal billing otherwise. No client-side param needed.\n  subscription: protectedProcedure.query(async ({ ctx }) => {\n    const referenceId = ctx.session.activeOrganizationId ?? ctx.user.id;\n\n    const sub = await ctx.db.query.subscription.findFirst({\n      where: (s, { eq, and, inArray }) =>\n        and(\n          eq(s.referenceId, referenceId),\n          inArray(s.status, [\"active\", \"trialing\"]),\n        ),\n    });\n\n    const plan = sub?.plan ?? \"free\";\n\n    if (!(plan in planLimits)) {\n      throw new Error(`Unknown plan \"${plan}\"`);\n    }\n\n    return {\n      plan,\n      status: sub?.status ?? null,\n      periodEnd: sub?.periodEnd ?? null,\n      cancelAtPeriodEnd: sub?.cancelAtPeriodEnd ?? false,\n      limits: planLimits[plan as PlanName],\n    };\n  }),\n});\n"
  },
  {
    "path": "apps/api/routers/organization.ts",
    "content": "import { z } from \"zod\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const organizationRouter = router({\n  list: protectedProcedure.query(() => {\n    // TODO: Implement organization listing logic\n    return {\n      organizations: [],\n    };\n  }),\n\n  create: protectedProcedure\n    .input(\n      z.object({\n        name: z.string().min(1),\n        description: z.string().optional(),\n      }),\n    )\n    .mutation(({ input, ctx }) => {\n      // TODO: Implement organization creation logic\n      return {\n        id: \"org_\" + Date.now(),\n        name: input.name,\n        description: input.description,\n        ownerId: ctx.user.id,\n      };\n    }),\n\n  update: protectedProcedure\n    .input(\n      z.object({\n        id: z.string(),\n        name: z.string().min(1).optional(),\n        description: z.string().optional(),\n      }),\n    )\n    .mutation(({ input }) => {\n      // TODO: Implement organization update logic\n      return {\n        ...input,\n      };\n    }),\n\n  delete: protectedProcedure\n    .input(z.object({ id: z.string() }))\n    .mutation(({ input }) => {\n      // TODO: Implement organization deletion logic\n      return { success: true, id: input.id };\n    }),\n\n  members: protectedProcedure\n    .input(z.object({ organizationId: z.string() }))\n    .query(() => {\n      // TODO: Implement organization members listing\n      return {\n        members: [],\n      };\n    }),\n\n  invite: protectedProcedure\n    .input(\n      z.object({\n        organizationId: z.string(),\n        email: z.email({ error: \"Invalid email address\" }),\n        role: z.enum([\"admin\", \"member\"]).default(\"member\"),\n      }),\n    )\n    .mutation(() => {\n      // TODO: Implement organization invite logic\n      return {\n        success: true,\n        inviteId: \"invite_\" + Date.now(),\n      };\n    }),\n});\n"
  },
  {
    "path": "apps/api/routers/user.ts",
    "content": "import { z } from \"zod\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const userRouter = router({\n  me: protectedProcedure.query(async ({ ctx }) => {\n    // User is now directly available in context from Better Auth\n    return {\n      id: ctx.user.id,\n      email: ctx.user.email,\n      name: ctx.user.name,\n    };\n  }),\n\n  updateProfile: protectedProcedure\n    .input(\n      z.object({\n        name: z.string().min(1).optional(),\n        email: z.email({ error: \"Invalid email address\" }).optional(),\n      }),\n    )\n    .mutation(({ input, ctx }) => {\n      // TODO: Implement user profile update logic\n      return {\n        id: ctx.user.id,\n        ...input,\n      };\n    }),\n\n  list: protectedProcedure\n    .input(\n      z.object({\n        limit: z.number().min(1).max(100).default(10),\n        cursor: z.string().optional(),\n      }),\n    )\n    .query(() => {\n      // TODO: Implement user listing logic\n      return {\n        users: [],\n        nextCursor: null,\n      };\n    }),\n});\n"
  },
  {
    "path": "apps/api/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/node.jsonc\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"tsBuildInfoFile\": \"../../.cache/tsconfig/api.tsbuildinfo\",\n    \"types\": [\"@cloudflare/workers-types\", \"bun\"]\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"**/dist/**/*\", \"**/node_modules/**/*\"],\n  \"references\": [{ \"path\": \"../../packages/core\" }, { \"path\": \"../../db\" }]\n}\n"
  },
  {
    "path": "apps/api/vitest.config.ts",
    "content": "import { defineProject } from \"vitest/config\";\n\nexport default defineProject({});\n"
  },
  {
    "path": "apps/api/worker.ts",
    "content": "/**\n * @file Cloudflare Workers entrypoint.\n *\n * Initializes database and auth context, then mounts the core Hono app.\n */\n\nimport { Hono } from \"hono\";\nimport { logger } from \"hono/logger\";\nimport { requestId } from \"hono/request-id\";\nimport { secureHeaders } from \"hono/secure-headers\";\nimport app from \"./lib/app.js\";\nimport { createAuth } from \"./lib/auth.js\";\nimport type { AppContext } from \"./lib/context.js\";\nimport { createDb } from \"./lib/db.js\";\nimport type { Env } from \"./lib/env.js\";\nimport {\n  errorHandler,\n  notFoundHandler,\n  requestIdGenerator,\n} from \"./lib/middleware.js\";\n\ntype CloudflareEnv = {\n  HYPERDRIVE_CACHED: Hyperdrive;\n  HYPERDRIVE_DIRECT: Hyperdrive;\n} & Env;\n\nconst worker = new Hono<{\n  Bindings: CloudflareEnv;\n  Variables: AppContext[\"Variables\"];\n}>();\n\n// Error and 404 handlers (must be on top-level app)\nworker.onError(errorHandler);\nworker.notFound(notFoundHandler);\n\n// Standard middleware\nworker.use(secureHeaders());\nworker.use(requestId({ generator: requestIdGenerator }));\nworker.use(logger());\n\n// Initialize shared context for all requests\nworker.use(async (c, next) => {\n  const db = createDb(c.env.HYPERDRIVE_CACHED);\n  const dbDirect = createDb(c.env.HYPERDRIVE_DIRECT);\n  const auth = createAuth(db, c.env);\n\n  c.set(\"db\", db);\n  c.set(\"dbDirect\", dbDirect);\n  c.set(\"auth\", auth);\n\n  await next();\n});\n\n// Mount the core API app\nworker.route(\"/\", app);\n\nexport default worker;\n"
  },
  {
    "path": "apps/api/wrangler.jsonc",
    "content": "{\n  \"$schema\": \"../../node_modules/wrangler/config-schema.json\",\n\n  // [METADATA]\n  // API worker accessed via service binding from web worker (no routes needed).\n  \"name\": \"example-api\",\n  \"main\": \"./worker.ts\",\n  \"compatibility_date\": \"2025-08-15\",\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"workers_dev\": false,\n  \"upload_source_maps\": true,\n\n  // [ENV:PRODUCTION]\n  // Command: bun wrangler deploy\n  \"vars\": {\n    \"ENVIRONMENT\": \"production\",\n    \"APP_NAME\": \"Example\",\n    \"APP_ORIGIN\": \"https://example.com\",\n    \"ALLOWED_ORIGINS\": \"https://example.com\",\n    \"RESEND_EMAIL_FROM\": \"onboarding@resend.dev\"\n  },\n  // prettier-ignore\n  \"hyperdrive\": [\n    { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-hyperdrive-cached-id-here\" },\n    { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-hyperdrive-direct-id-here\" }\n  ],\n  \"kv_namespaces\": [],\n\n  \"env\": {\n    // [ENV:DEVELOPMENT]\n    // Command: bun wrangler dev\n    \"dev\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"development\",\n        \"APP_NAME\": \"Example\",\n        \"APP_ORIGIN\": \"http://localhost:5173\",\n        \"ALLOWED_ORIGINS\": \"http://localhost:5173,http://127.0.0.1:5173\",\n        \"RESEND_EMAIL_FROM\": \"onboarding@resend.dev\"\n      },\n      // prettier-ignore\n      \"hyperdrive\": [\n        { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-dev-hyperdrive-cached-id-here\" },\n        { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-dev-hyperdrive-direct-id-here\" }\n      ],\n      \"kv_namespaces\": []\n    },\n\n    // [ENV:STAGING]\n    // Command: bun wrangler deploy --env staging\n    \"staging\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"staging\",\n        \"APP_NAME\": \"Example\",\n        \"APP_ORIGIN\": \"https://staging.example.com\",\n        \"ALLOWED_ORIGINS\": \"https://staging.example.com\",\n        \"RESEND_EMAIL_FROM\": \"onboarding@resend.dev\"\n      },\n      // prettier-ignore\n      \"hyperdrive\": [\n        { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-staging-hyperdrive-cached-id-here\" },\n        { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-staging-hyperdrive-direct-id-here\" }\n      ],\n      \"kv_namespaces\": []\n    },\n\n    // [ENV:PREVIEW]\n    // Command: bun wrangler deploy --env preview\n    \"preview\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"preview\",\n        \"APP_NAME\": \"Example\",\n        \"APP_ORIGIN\": \"https://preview.example.com\",\n        \"ALLOWED_ORIGINS\": \"https://preview.example.com\",\n        \"RESEND_EMAIL_FROM\": \"onboarding@resend.dev\"\n      },\n      // prettier-ignore\n      \"hyperdrive\": [\n        { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-preview-hyperdrive-cached-id-here\" },\n        { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-preview-hyperdrive-direct-id-here\" }\n      ],\n      \"kv_namespaces\": []\n    }\n  }\n}\n"
  },
  {
    "path": "apps/app/AGENTS.md",
    "content": "Client-side SPA — no SSR. All rendering happens in the browser.\n\n## Routing\n\n- File-based routing in `routes/`. `lib/routeTree.gen.ts` is auto-generated — never edit it.\n- Route groups: `(app)/` = protected, `(auth)/` = public. Parentheses don't affect URLs.\n- `route.tsx` in a group = layout with shared `beforeLoad`; individual files for pages.\n\n## Authentication\n\n- Session state via `useSessionQuery()` from `lib/queries/session.ts`. NEVER use `auth.useSession()` — TanStack Query provides caching, multi-tab sync, and consistency.\n- Auth guard in `beforeLoad`, not in components. Uses cache-first (`getCachedSession()`), then `fetchQuery()`.\n- Must validate both `user` AND `session` (not just one).\n- After login: call `revalidateSession(queryClient, router)` — removes cache + invalidates router so `beforeLoad` fetches fresh data, then navigate.\n- Safe redirects: use `getSafeRedirectUrl()` for `returnTo` search params (prevents open redirects).\n- `signOut(queryClient)` clears server session, invalidates cache, redirects to `/login`.\n\n## tRPC Client\n\n- `credentials: \"include\"` for cookie-based auth, batched via `httpBatchLink`.\n- API URL: `${import.meta.env.VITE_API_URL || \"/api\"}/trpc`.\n- Uses `createTRPCOptionsProxy()` for TanStack Query integration.\n\n## Components\n\n- Named exports, functional only. shadcn/ui from `@repo/ui`.\n- Navigation: `<Link>` from TanStack Router with `activeProps` for active styling. Never use `<a>` for internal routes.\n- Route context: `Route.useSearch()` for search params, `Route.useRouteContext()` for route data.\n- Jotai store available for cross-route UI state (modals, sidebar).\n\n## Error Handling\n\n- `AppErrorBoundary` (root) shows generic error UI. `AuthErrorBoundary` (protected routes) catches 401/UNAUTHORIZED and shows sign-in recovery UI; 403 falls through to generic handler.\n- Utilities in `lib/errors.ts`: `getErrorStatus()`, `isUnauthenticatedError()`, `getErrorMessage()`.\n"
  },
  {
    "path": "apps/app/README.md",
    "content": "# React Application\n\nSingle-page application built with React 19, TanStack Router, Jotai, shadcn/ui, and Tailwind CSS v4.\n\n[Documentation](https://reactstarter.com/frontend/routing) | [Getting Started](https://reactstarter.com/getting-started/quick-start)\n\n## Development\n\n```bash\nbun app:dev       # Start dev server (http://localhost:5173)\nbun app:build     # Build for production\nbun app:deploy    # Deploy to Cloudflare Workers\n```\n\n## Structure\n\n```bash\nroutes/           # File-based routes (TanStack Router)\ncomponents/       # Shared app components\nlib/              # Auth client, tRPC client, Jotai atoms, utilities\nstyles/           # Global CSS and theme variables\n```\n\nRoute tree is auto-generated in `lib/routeTree.gen.ts` -- do not edit manually.\n"
  },
  {
    "path": "apps/app/components/auth/auth-error-boundary.tsx",
    "content": "import { getErrorMessage, isUnauthenticatedError } from \"@/lib/errors\";\nimport { sessionQueryKey } from \"@/lib/queries/session\";\nimport { Button } from \"@repo/ui\";\nimport {\n  useQueryClient,\n  useQueryErrorResetBoundary,\n} from \"@tanstack/react-query\";\nimport { AlertCircle } from \"lucide-react\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\ninterface ResetProps {\n  resetErrorBoundary: () => void;\n}\n\n// Fallback for auth errors in protected routes\nfunction AuthErrorFallback({ resetErrorBoundary }: ResetProps) {\n  const queryClient = useQueryClient();\n\n  const handleRetry = () => {\n    queryClient.resetQueries({ queryKey: sessionQueryKey });\n    resetErrorBoundary();\n  };\n\n  const handleSignIn = () => {\n    queryClient.removeQueries({ queryKey: sessionQueryKey });\n    const { pathname, search, hash } = window.location;\n    const returnTo = encodeURIComponent(pathname + search + hash);\n    window.location.href = `/login?returnTo=${returnTo}`;\n  };\n\n  return (\n    <div className=\"flex min-h-svh flex-col items-center justify-center p-6\">\n      <div className=\"mx-auto max-w-md text-center\">\n        <AlertCircle className=\"mx-auto mb-4 h-12 w-12 text-destructive\" />\n        <h1 className=\"mb-2 text-2xl font-bold\">Authentication Required</h1>\n        <p className=\"mb-6 text-muted-foreground\">\n          Please sign in to access this page.\n        </p>\n        <div className=\"flex justify-center gap-3\">\n          <Button variant=\"outline\" onClick={handleRetry}>\n            Try Again\n          </Button>\n          <Button onClick={handleSignIn}>Sign In</Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface ErrorFallbackProps {\n  error: unknown;\n  resetErrorBoundary: () => void;\n}\n\n// Generic error fallback for non-auth errors\nfunction GenericErrorFallback({\n  error,\n  resetErrorBoundary,\n}: ErrorFallbackProps) {\n  return (\n    <div className=\"flex min-h-svh flex-col items-center justify-center p-6\">\n      <div className=\"mx-auto max-w-md text-center\">\n        <AlertCircle className=\"mx-auto mb-4 h-12 w-12 text-destructive\" />\n        <h1 className=\"mb-2 text-2xl font-bold\">Something went wrong</h1>\n        <p className=\"mb-6 text-muted-foreground\">{getErrorMessage(error)}</p>\n        <Button onClick={resetErrorBoundary}>Try Again</Button>\n      </div>\n    </div>\n  );\n}\n\ninterface ErrorBoundaryProps {\n  children: React.ReactNode;\n}\n\n// Routes auth errors to AuthErrorFallback, others to GenericErrorFallback\nfunction AuthAwareErrorFallback({\n  error,\n  resetErrorBoundary,\n}: ErrorFallbackProps) {\n  return isUnauthenticatedError(error) ? (\n    <AuthErrorFallback resetErrorBoundary={resetErrorBoundary} />\n  ) : (\n    <GenericErrorFallback\n      error={error}\n      resetErrorBoundary={resetErrorBoundary}\n    />\n  );\n}\n\n// Auth error boundary for protected routes only.\n// Catches auth errors (tRPC UNAUTHORIZED or HTTP 401) and shows recovery UI.\n// 403 (forbidden) falls through to generic handler since user IS authenticated.\nexport function AuthErrorBoundary({ children }: ErrorBoundaryProps) {\n  const queryClient = useQueryClient();\n  const { reset } = useQueryErrorResetBoundary();\n\n  return (\n    <ErrorBoundary\n      FallbackComponent={AuthAwareErrorFallback}\n      onReset={reset}\n      onError={(error) => {\n        console.error(\"Error caught by boundary:\", error);\n        if (isUnauthenticatedError(error)) {\n          queryClient.removeQueries({ queryKey: sessionQueryKey });\n        }\n      }}\n    >\n      {children}\n    </ErrorBoundary>\n  );\n}\n\n// Generic error boundary for app root - no auth-specific handling\nexport function AppErrorBoundary({ children }: ErrorBoundaryProps) {\n  const { reset } = useQueryErrorResetBoundary();\n\n  return (\n    <ErrorBoundary\n      FallbackComponent={GenericErrorFallback}\n      onReset={reset}\n      onError={(error) => console.error(\"Uncaught error:\", error)}\n    >\n      {children}\n    </ErrorBoundary>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/auth/auth-form.tsx",
    "content": "import { Button, Input, cn } from \"@repo/ui\";\nimport { Link } from \"@tanstack/react-router\";\nimport { ArrowLeft, Mail } from \"lucide-react\";\nimport type { ComponentProps, FormEvent } from \"react\";\nimport { GoogleLogin } from \"./google-login\";\nimport { OtpVerification } from \"./otp-verification\";\nimport { PasskeyLogin } from \"./passkey-login\";\nimport { useAuthForm } from \"./use-auth-form\";\n\nconst APP_NAME = import.meta.env.VITE_APP_NAME || \"your account\";\n\nfunction SignupTerms() {\n  return (\n    <p className=\"text-xs text-muted-foreground text-center text-balance\">\n      By signing up, you agree to our{\" \"}\n      <a\n        href=\"/terms\"\n        className=\"underline underline-offset-4 hover:text-primary\"\n      >\n        Terms of Service\n      </a>{\" \"}\n      and{\" \"}\n      <a\n        href=\"/privacy\"\n        className=\"underline underline-offset-4 hover:text-primary\"\n      >\n        Privacy Policy\n      </a>\n      .\n    </p>\n  );\n}\n\ninterface AuthFormProps extends ComponentProps<\"div\"> {\n  /**\n   * UI mode affecting copy, ToS display, and available methods.\n   * Both modes use the same passwordless OTP flow that auto-creates accounts.\n   */\n  mode?: \"login\" | \"signup\";\n  /** Called after successful auth. Awaited before UI progresses. Caller handles cache invalidation and navigation. */\n  onSuccess: () => Promise<void>;\n  isLoading?: boolean;\n  /** Post-auth redirect destination. Must be a safe relative path (validated by caller). */\n  returnTo?: string;\n}\n\nexport function AuthForm({\n  className,\n  onSuccess,\n  isLoading,\n  mode = \"login\",\n  returnTo,\n  ...props\n}: AuthFormProps) {\n  const {\n    step,\n    email,\n    isDisabled,\n    error,\n    changeEmail,\n    onAuthSuccess,\n    setError,\n    sendOtp,\n    goToEmailStep,\n    goToMethodStep,\n    resetToEmail,\n    setChildBusy,\n    mode: formMode,\n  } = useAuthForm({\n    onSuccess,\n    isExternallyLoading: isLoading,\n    mode,\n  });\n\n  // Clear error when user changes email\n  const handleEmailChange = (value: string) => {\n    if (error) setError(null);\n    changeEmail(value);\n  };\n\n  // Voluntary back from OTP clears error; forced back (via onCancel) preserves it\n  const handleOtpBack = () => {\n    setError(null);\n    resetToEmail();\n  };\n\n  const isSignup = formMode === \"signup\";\n\n  return (\n    <div className={cn(\"flex flex-col gap-6 w-full\", className)} {...props}>\n      {/* Logo */}\n      <div className=\"flex justify-center\">\n        <Link to=\"/\" aria-label=\"Go to homepage\">\n          <img src=\"/logo512.png\" alt=\"\" className=\"h-10 w-10\" />\n        </Link>\n      </div>\n\n      {/* Error message - role=\"alert\" ensures screen readers announce it */}\n      {error && (\n        <div\n          role=\"alert\"\n          className=\"rounded-md bg-destructive/10 p-3 text-sm text-destructive\"\n        >\n          {error}\n        </div>\n      )}\n\n      {/* Step: Method Selection */}\n      {step === \"method\" && (\n        <MethodSelection\n          isSignup={isSignup}\n          isDisabled={isDisabled}\n          onEmailClick={goToEmailStep}\n          onSuccess={onAuthSuccess}\n          onError={setError}\n          onLoadingChange={setChildBusy}\n          returnTo={returnTo}\n        />\n      )}\n\n      {/* Step: Email Input */}\n      {step === \"email\" && (\n        <EmailInput\n          email={email}\n          isSignup={isSignup}\n          isDisabled={isDisabled}\n          onEmailChange={handleEmailChange}\n          onSubmit={sendOtp}\n          onBack={goToMethodStep}\n        />\n      )}\n\n      {/* Step: OTP Verification */}\n      {step === \"otp\" && (\n        <OtpStep\n          email={email}\n          isDisabled={isDisabled}\n          onSuccess={onAuthSuccess}\n          onError={setError}\n          onLoadingChange={setChildBusy}\n          onBack={handleOtpBack}\n          onCancel={resetToEmail}\n        />\n      )}\n    </div>\n  );\n}\n\n// Step 1: Method Selection\ninterface MethodSelectionProps {\n  isSignup: boolean;\n  isDisabled: boolean;\n  onEmailClick: () => void;\n  onSuccess: () => void;\n  onError: (error: string | null) => void;\n  onLoadingChange: (loading: boolean) => void;\n  returnTo?: string;\n}\n\nfunction MethodSelection({\n  isSignup,\n  isDisabled,\n  onEmailClick,\n  onSuccess,\n  onError,\n  onLoadingChange,\n  returnTo,\n}: MethodSelectionProps) {\n  const heading = isSignup ? \"Create your account\" : `Log in to ${APP_NAME}`;\n\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <h1 className=\"text-2xl font-bold text-center\">{heading}</h1>\n\n      <div className=\"flex flex-col gap-3\">\n        <GoogleLogin\n          onError={onError}\n          isDisabled={isDisabled}\n          onLoadingChange={onLoadingChange}\n          returnTo={returnTo}\n        />\n\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          className=\"w-full\"\n          onClick={onEmailClick}\n          disabled={isDisabled}\n        >\n          <Mail className=\"mr-2 h-4 w-4\" />\n          Continue with email\n        </Button>\n\n        {/* Passkey only available for login (requires existing account) */}\n        {!isSignup && (\n          <PasskeyLogin\n            onSuccess={onSuccess}\n            onError={onError}\n            onLoadingChange={onLoadingChange}\n            isDisabled={isDisabled}\n          />\n        )}\n      </div>\n\n      {isSignup && <SignupTerms />}\n\n      {/* Account switch link */}\n      <p className=\"text-sm text-muted-foreground text-center\">\n        {isSignup ? (\n          <>\n            Already have an account?{\" \"}\n            <Link\n              to=\"/login\"\n              className=\"font-medium text-primary underline-offset-4 hover:underline\"\n            >\n              Log in\n            </Link>\n          </>\n        ) : (\n          <>\n            Don't have an account?{\" \"}\n            <Link\n              to=\"/signup\"\n              className=\"font-medium text-primary underline-offset-4 hover:underline\"\n            >\n              Sign up\n            </Link>\n          </>\n        )}\n      </p>\n    </div>\n  );\n}\n\n// Step 2: Email Input\ninterface EmailInputProps {\n  email: string;\n  isSignup: boolean;\n  isDisabled: boolean;\n  onEmailChange: (email: string) => void;\n  onSubmit: (e?: FormEvent) => void;\n  onBack: () => void;\n}\n\nfunction EmailInput({\n  email,\n  isSignup,\n  isDisabled,\n  onEmailChange,\n  onSubmit,\n  onBack,\n}: EmailInputProps) {\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <h1 className=\"text-2xl font-bold text-center\">\n        What's your email address?\n      </h1>\n\n      <form onSubmit={onSubmit} className=\"flex flex-col gap-3\">\n        <Input\n          type=\"email\"\n          placeholder=\"Enter your email address...\"\n          value={email}\n          onChange={(e) => onEmailChange(e.target.value)}\n          disabled={isDisabled}\n          autoComplete=\"email\"\n          autoFocus\n          required\n        />\n        <Button\n          type=\"submit\"\n          variant=\"default\"\n          className=\"w-full\"\n          disabled={isDisabled || !email.trim()}\n        >\n          Continue with email\n        </Button>\n      </form>\n\n      {isSignup && <SignupTerms />}\n\n      {/* Back link */}\n      <button\n        type=\"button\"\n        onClick={onBack}\n        disabled={isDisabled}\n        className=\"flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50\"\n      >\n        <ArrowLeft className=\"h-4 w-4\" />\n        Back to {isSignup ? \"sign up\" : \"login\"}\n      </button>\n    </div>\n  );\n}\n\n// Step 3: OTP Verification\ninterface OtpStepProps {\n  email: string;\n  isDisabled: boolean;\n  onSuccess: () => void;\n  onError: (error: string | null) => void;\n  onLoadingChange: (loading: boolean) => void;\n  onBack: () => void;\n  onCancel: () => void;\n}\n\nfunction OtpStep({\n  email,\n  isDisabled,\n  onSuccess,\n  onError,\n  onLoadingChange,\n  onBack,\n  onCancel,\n}: OtpStepProps) {\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <div className=\"text-center\">\n        <h1 className=\"text-2xl font-bold\">Check your email</h1>\n        <p className=\"text-muted-foreground mt-1\">\n          We sent a code to <strong>{email}</strong>\n        </p>\n      </div>\n\n      <OtpVerification\n        email={email}\n        onSuccess={onSuccess}\n        onError={onError}\n        onLoadingChange={onLoadingChange}\n        onCancel={onCancel}\n        isDisabled={isDisabled}\n      />\n\n      {/* Back link */}\n      <button\n        type=\"button\"\n        onClick={onBack}\n        disabled={isDisabled}\n        className=\"flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50\"\n      >\n        <ArrowLeft className=\"h-4 w-4\" />\n        Back to email\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/auth/google-login.tsx",
    "content": "import { auth } from \"@/lib/auth\";\nimport { sessionQueryKey } from \"@/lib/queries/session\";\nimport { Button } from \"@repo/ui\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useCallback, useState } from \"react\";\n\ninterface GoogleLoginProps {\n  onError: (error: string | null) => void;\n  isDisabled?: boolean;\n  onLoadingChange?: (loading: boolean) => void;\n  /** Post-auth redirect destination (already validated by caller). */\n  returnTo?: string;\n}\n\nexport function GoogleLogin({\n  onError,\n  isDisabled,\n  onLoadingChange,\n  returnTo,\n}: GoogleLoginProps) {\n  const queryClient = useQueryClient();\n  const [isLoading, setIsLoading] = useState(false);\n\n  const setLoading = useCallback(\n    (loading: boolean) => {\n      setIsLoading(loading);\n      onLoadingChange?.(loading);\n    },\n    [onLoadingChange],\n  );\n\n  const handleGoogleLogin = async () => {\n    setLoading(true);\n    onError(null);\n\n    try {\n      // Clear stale session before OAuth redirect\n      queryClient.removeQueries({ queryKey: sessionQueryKey });\n\n      // OAuth redirects to /login which validates session and redirects to returnTo\n      const callbackURL = returnTo\n        ? `/login?returnTo=${encodeURIComponent(returnTo)}`\n        : \"/login\";\n\n      const result = await auth.signIn.social({\n        provider: \"google\",\n        callbackURL,\n      });\n\n      if (result?.error) {\n        onError(result.error.message || \"Failed to sign in with Google\");\n        setLoading(false);\n      } else if (!result?.data?.redirect) {\n        // No redirect (popup blocked, misconfigured provider, etc.) - reset loading\n        setLoading(false);\n      }\n      // On redirect, page navigates away - component unmounts, no cleanup needed\n    } catch (err) {\n      console.error(\"Google login error:\", err);\n      onError(\"Failed to sign in with Google\");\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      className=\"w-full\"\n      onClick={handleGoogleLogin}\n      disabled={isDisabled || isLoading}\n    >\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        className=\"mr-2 h-4 w-4\"\n      >\n        <path\n          d=\"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z\"\n          fill=\"currentColor\"\n        />\n      </svg>\n      Continue with Google\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/auth/index.ts",
    "content": "export { AppErrorBoundary, AuthErrorBoundary } from \"./auth-error-boundary\";\nexport { AuthForm } from \"./auth-form\";\nexport { LoginDialog, useLoginDialog } from \"./login-dialog\";\nexport { OtpVerification } from \"./otp-verification\";\nexport { PasskeyLogin } from \"./passkey-login\";\nexport { GoogleLogin } from \"./google-login\";\nexport { useAuthForm, type AuthStep } from \"./use-auth-form\";\n"
  },
  {
    "path": "apps/app/components/auth/login-dialog.tsx",
    "content": "import { getSafeRedirectUrl } from \"@/lib/auth-config\";\nimport { revalidateSession } from \"@/lib/queries/session\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@repo/ui\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useRouter, useRouterState } from \"@tanstack/react-router\";\nimport { useState } from \"react\";\nimport { AuthForm } from \"./auth-form\";\n\ninterface LoginDialogProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Login dialog component for modal authentication.\n * Use with useLoginDialog hook for programmatic control.\n */\nexport function LoginDialog({ open, onOpenChange }: LoginDialogProps) {\n  const router = useRouter();\n  const queryClient = useQueryClient();\n\n  // Preserve full URL (pathname + search + hash) for OAuth redirect\n  const returnTo = useRouterState({\n    select: (s) => {\n      const { pathname, search, hash } = s.location;\n      return getSafeRedirectUrl(pathname + search + hash);\n    },\n  });\n\n  async function handleSuccess() {\n    await revalidateSession(queryClient, router);\n    onOpenChange(false);\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader className=\"sr-only\">\n          <DialogTitle>Sign in to your account</DialogTitle>\n          <DialogDescription>\n            Choose your preferred sign in method\n          </DialogDescription>\n        </DialogHeader>\n        <AuthForm mode=\"login\" onSuccess={handleSuccess} returnTo={returnTo} />\n      </DialogContent>\n    </Dialog>\n  );\n}\n\n/**\n * Hook for programmatically controlling the login dialog.\n *\n * @example\n * ```tsx\n * function App() {\n *   const loginDialog = useLoginDialog();\n *\n *   return (\n *     <>\n *       <button onClick={loginDialog.open}>Sign In</button>\n *       <LoginDialog {...loginDialog.props} />\n *     </>\n *   );\n * }\n * ```\n */\nexport function useLoginDialog() {\n  const [isOpen, setIsOpen] = useState(false);\n\n  return {\n    isOpen,\n    open: () => setIsOpen(true),\n    close: () => setIsOpen(false),\n    props: {\n      open: isOpen,\n      onOpenChange: setIsOpen,\n    },\n  };\n}\n"
  },
  {
    "path": "apps/app/components/auth/otp-verification.tsx",
    "content": "import { auth } from \"@/lib/auth\";\nimport { Button, Input } from \"@repo/ui\";\nimport type { FormEvent } from \"react\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nconst RESEND_COOLDOWN_SECONDS = 30;\n\n// Better Auth email-otp plugin error codes (matches server-side ERROR_CODES)\nconst OTP_ERROR_CODES = {\n  TOO_MANY_ATTEMPTS: \"TOO_MANY_ATTEMPTS\",\n  OTP_EXPIRED: \"OTP_EXPIRED\",\n  INVALID_OTP: \"INVALID_OTP\",\n} as const;\n\ninterface OtpVerificationProps {\n  email: string;\n  onSuccess: () => void;\n  onError: (error: string | null) => void;\n  onLoadingChange?: (loading: boolean) => void;\n  onCancel: () => void;\n  isDisabled?: boolean;\n}\n\nexport function OtpVerification({\n  email,\n  onSuccess,\n  onError,\n  onLoadingChange,\n  onCancel,\n  isDisabled,\n}: OtpVerificationProps) {\n  const [otp, setOtp] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [resendCooldown, setResendCooldown] = useState(0);\n\n  const setLoading = useCallback(\n    (loading: boolean) => {\n      setIsLoading(loading);\n      onLoadingChange?.(loading);\n    },\n    [onLoadingChange],\n  );\n\n  // Countdown timer for resend cooldown\n  useEffect(() => {\n    if (resendCooldown <= 0) return;\n    const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000);\n    return () => clearTimeout(timer);\n  }, [resendCooldown]);\n\n  const handleOtpVerification = async (e: FormEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (!email || !otp) return;\n\n    try {\n      setLoading(true);\n      onError(null);\n\n      const result = await auth.signIn.emailOtp({\n        email,\n        otp,\n      });\n\n      if (result.data) {\n        onSuccess();\n      } else if (result.error) {\n        const code = \"code\" in result.error ? result.error.code : undefined;\n        if (code === OTP_ERROR_CODES.TOO_MANY_ATTEMPTS) {\n          onError(\"Too many failed attempts. Please request a new code.\");\n          onCancel();\n        } else if (code === OTP_ERROR_CODES.OTP_EXPIRED) {\n          onError(\"Code has expired. Please request a new one.\");\n          onCancel();\n        } else {\n          onError(result.error.message || \"Invalid verification code\");\n        }\n      }\n    } catch (err) {\n      console.error(\"OTP verification error:\", err);\n      onError(\"Failed to verify code\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleResendOtp = async () => {\n    if (resendCooldown > 0) return;\n\n    setOtp(\"\");\n    onError(null);\n\n    try {\n      setLoading(true);\n\n      // \"sign-in\" type handles both login and signup (creates user if needed)\n      const result = await auth.emailOtp.sendVerificationOtp({\n        email,\n        type: \"sign-in\",\n      });\n\n      if (result.error) {\n        onError(result.error.message || \"Failed to send OTP\");\n      } else {\n        setResendCooldown(RESEND_COOLDOWN_SECONDS);\n      }\n    } catch (err) {\n      console.error(\"Email OTP error:\", err);\n      onError(\"Failed to send verification code\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const disabled = isDisabled || isLoading;\n\n  return (\n    <form onSubmit={handleOtpVerification} className=\"flex flex-col gap-3\">\n      <Input\n        type=\"text\"\n        placeholder=\"Enter 6-digit code\"\n        value={otp}\n        onChange={(e) => setOtp(e.target.value.replace(/\\D/g, \"\").slice(0, 6))}\n        disabled={disabled}\n        autoComplete=\"one-time-code\"\n        required\n        autoFocus\n        maxLength={6}\n        pattern=\"[0-9]{6}\"\n        inputMode=\"numeric\"\n        className=\"text-center text-lg tracking-widest\"\n      />\n      <Button\n        type=\"submit\"\n        variant=\"default\"\n        className=\"w-full\"\n        disabled={disabled || otp.length !== 6}\n      >\n        Verify code\n      </Button>\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        className=\"w-full text-sm\"\n        onClick={handleResendOtp}\n        disabled={disabled || resendCooldown > 0}\n      >\n        {resendCooldown > 0\n          ? `Resend code in ${resendCooldown}s`\n          : \"Resend code\"}\n      </Button>\n    </form>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/auth/passkey-login.tsx",
    "content": "import { auth } from \"@/lib/auth\";\nimport { authConfig } from \"@/lib/auth-config\";\nimport { Button } from \"@repo/ui\";\nimport { KeyRound } from \"lucide-react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\ninterface PasskeyLoginProps {\n  onSuccess: () => void;\n  onError: (error: string | null) => void;\n  onLoadingChange?: (loading: boolean) => void;\n  isDisabled?: boolean;\n}\n\n/**\n * Passkey sign-in component using WebAuthn.\n *\n * WebAuthn handles credential discovery - no email input needed.\n * The browser prompts the user to select from their available passkeys.\n */\nexport function PasskeyLogin({\n  onSuccess,\n  onError,\n  onLoadingChange,\n  isDisabled,\n}: PasskeyLoginProps) {\n  const [isLoading, setIsLoading] = useState(false);\n\n  const setLoading = useCallback(\n    (loading: boolean) => {\n      setIsLoading(loading);\n      onLoadingChange?.(loading);\n    },\n    [onLoadingChange],\n  );\n\n  const onSuccessRef = useRef(onSuccess);\n  onSuccessRef.current = onSuccess;\n\n  // Set up conditional UI for passkey autofill (gated by config)\n  useEffect(() => {\n    if (!authConfig.passkey.enableConditionalUI) return;\n\n    let aborted = false;\n\n    const setupConditionalUI = async () => {\n      try {\n        if (!window.PublicKeyCredential?.isConditionalMediationAvailable)\n          return;\n\n        const isAvailable =\n          await window.PublicKeyCredential.isConditionalMediationAvailable();\n        if (!isAvailable) return;\n\n        // Enable autofill for passkeys on input fields with autocomplete=\"webauthn\"\n        const result = await auth.signIn.passkey({ autoFill: true });\n        if (result.data && !aborted) {\n          onSuccessRef.current();\n        }\n      } catch {\n        // Silently ignore errors from conditional UI (user hasn't explicitly requested auth)\n      }\n    };\n\n    setupConditionalUI();\n\n    return () => {\n      aborted = true;\n    };\n  }, []);\n\n  const handlePasskeyLogin = async () => {\n    // Check WebAuthn support before attempting\n    if (!window.PublicKeyCredential) {\n      onError(authConfig.errors.passkeyNotSupported);\n      return;\n    }\n\n    setLoading(true);\n    onError(null);\n\n    try {\n      // Better Auth passkey client returns errors via result.error for HTTP/WebAuthn errors,\n      // but network failures (offline, DNS) can still reject\n      const result = await auth.signIn.passkey();\n\n      if (result.data) {\n        onSuccess();\n      } else if (result.error) {\n        // AUTH_CANCELLED: user dismissed prompt, timed out, or WebAuthn not supported\n        // Server errors (e.g., no passkey found) have different codes\n        const errorCode =\n          \"code\" in result.error ? result.error.code : undefined;\n        if (errorCode === \"AUTH_CANCELLED\") {\n          onError(\"Passkey authentication was cancelled.\");\n        } else {\n          onError(result.error.message || authConfig.errors.genericError);\n        }\n      }\n    } catch {\n      // Network-level failures (offline, DNS, connection refused)\n      onError(authConfig.errors.networkError);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <Button\n      type=\"button\"\n      variant=\"default\"\n      className=\"w-full\"\n      onClick={handlePasskeyLogin}\n      disabled={isDisabled || isLoading}\n    >\n      <KeyRound className=\"mr-2 h-4 w-4\" />\n      Log in with passkey\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/auth/use-auth-form.ts",
    "content": "import { auth } from \"@/lib/auth\";\nimport type { FormEvent } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\n\nexport type AuthStep = \"method\" | \"email\" | \"otp\";\n\n// Minimal state machine for passwordless OTP flow. Intentionally shallow:\n// - Errors are orthogonal to steps (can occur at any step)\n// - No terminal state (component unmounts on success)\n// Revisit if adding password fallback or MFA steps.\nconst VALID_TRANSITIONS: Record<AuthStep, AuthStep[]> = {\n  method: [\"email\"],\n  email: [\"method\", \"otp\"],\n  otp: [\"email\"],\n};\n\ninterface UseAuthFormOptions {\n  /**\n   * Called after successful authentication. Caller is responsible for\n   * cache invalidation and navigation. Awaited before form state resets.\n   */\n  onSuccess: () => Promise<void>;\n  isExternallyLoading?: boolean;\n  /**\n   * UI mode affecting copy, ToS display, and available methods.\n   * Both modes use the same passwordless OTP flow that auto-creates accounts.\n   */\n  mode?: \"login\" | \"signup\";\n}\n\nexport function useAuthForm({\n  onSuccess,\n  isExternallyLoading,\n  mode = \"login\",\n}: UseAuthFormOptions) {\n  const [step, setStep] = useState<AuthStep>(\"method\");\n  const [email, setEmail] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  // Counter-based to handle overlapping child operations (e.g., rapid double-click)\n  const [pendingOps, setPendingOps] = useState(0);\n  const [error, setError] = useState<string | null>(null);\n\n  // Guards against concurrent auth completion (e.g., passkey conditional UI + manual click).\n  // Conditional passkey autofill intentionally doesn't block UI - it's passive/background.\n  // Reset when returning to method step to allow retry after navigation back.\n  const hasSucceededRef = useRef(false);\n  // Ref provides current step to memoized transitionTo callback (avoids stale closure)\n  const stepRef = useRef(step);\n  stepRef.current = step;\n\n  // Track child loading via counter to correctly handle overlapping operations\n  const setChildBusy = useCallback((busy: boolean) => {\n    setPendingOps((c) => (busy ? c + 1 : Math.max(0, c - 1)));\n  }, []);\n\n  // Unified busy state: disables navigation and other auth methods while any flow is active\n  const isDisabled = isLoading || pendingOps > 0 || !!isExternallyLoading;\n\n  const onAuthSuccess = async () => {\n    if (hasSucceededRef.current) return;\n    hasSucceededRef.current = true;\n\n    try {\n      setIsLoading(true);\n      await onSuccess();\n    } catch (err) {\n      console.error(\"Post-auth error:\", err);\n      setError(\"Something went wrong. Please try again.\");\n      hasSucceededRef.current = false; // Allow retry on error\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  // Validates transitions to prevent invalid step jumps.\n  // Returning to \"method\" resets the success guard to allow fresh auth attempts.\n  const transitionTo = useCallback((next: AuthStep, clearErr = true) => {\n    const current = stepRef.current;\n    if (!VALID_TRANSITIONS[current].includes(next)) {\n      return;\n    }\n    if (next === \"method\") {\n      hasSucceededRef.current = false;\n    }\n    setStep(next);\n    if (clearErr) setError(null);\n  }, []);\n\n  const goToEmailStep = () => transitionTo(\"email\");\n  const goToMethodStep = () => transitionTo(\"method\");\n  // Go back to email step, preserving error message\n  const resetToEmail = () => transitionTo(\"email\", false);\n\n  const sendOtp = async (e?: FormEvent) => {\n    e?.preventDefault();\n\n    // Normalize before auth calls to prevent case/whitespace mismatches\n    const normalizedEmail = email.trim().toLowerCase();\n    if (!normalizedEmail) return;\n    setEmail(normalizedEmail);\n\n    try {\n      setIsLoading(true);\n      setError(null);\n\n      // \"sign-in\" type handles both login and signup (creates user if needed)\n      const result = await auth.emailOtp.sendVerificationOtp({\n        email: normalizedEmail,\n        type: \"sign-in\",\n      });\n\n      if (result.data) {\n        transitionTo(\"otp\");\n      } else if (result.error) {\n        setError(result.error.message || \"Failed to send OTP\");\n      }\n    } catch (err) {\n      console.error(\"Email OTP error:\", err);\n      setError(\"Failed to send verification code\");\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const changeEmail = (value: string) => {\n    setEmail(value);\n  };\n\n  return {\n    // State\n    step,\n    email,\n    isLoading,\n    isDisabled,\n    error,\n    mode,\n\n    // Actions\n    changeEmail,\n    onAuthSuccess,\n    setError,\n    sendOtp,\n    goToEmailStep,\n    goToMethodStep,\n    resetToEmail,\n    setChildBusy,\n  };\n}\n"
  },
  {
    "path": "apps/app/components/index.ts",
    "content": "export { NotFound } from \"./not-found\";\n"
  },
  {
    "path": "apps/app/components/layout/constants.ts",
    "content": "import { Activity, FileText, Home, Settings, Users } from \"lucide-react\";\n\nexport const sidebarItems = [\n  { icon: Home, label: \"Dashboard\", to: \"/\" },\n  { icon: Activity, label: \"Analytics\", to: \"/analytics\" },\n  { icon: Users, label: \"Users\", to: \"/users\" },\n  { icon: FileText, label: \"Reports\", to: \"/reports\" },\n  { icon: Settings, label: \"Settings\", to: \"/settings\" },\n] as const;\n"
  },
  {
    "path": "apps/app/components/layout/header.tsx",
    "content": "import { Button } from \"@repo/ui\";\nimport { Menu, Settings, X } from \"lucide-react\";\n\ninterface HeaderProps {\n  isSidebarOpen: boolean;\n  onMenuToggle: () => void;\n}\n\nexport function Header({ isSidebarOpen, onMenuToggle }: HeaderProps) {\n  return (\n    <header className=\"h-14 border-b bg-background flex items-center px-4 gap-4\">\n      <Button\n        variant=\"ghost\"\n        size=\"icon\"\n        onClick={onMenuToggle}\n        className=\"shrink-0\"\n      >\n        {isSidebarOpen ? (\n          <X className=\"h-5 w-5\" />\n        ) : (\n          <Menu className=\"h-5 w-5\" />\n        )}\n      </Button>\n\n      <div className=\"flex-1 flex items-center gap-4\">\n        <h1 className=\"text-lg font-semibold\">Application</h1>\n      </div>\n\n      <div className=\"flex items-center gap-2\">\n        <Button variant=\"ghost\" size=\"icon\">\n          <Settings className=\"h-5 w-5\" />\n        </Button>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/layout/index.tsx",
    "content": "import { useState } from \"react\";\nimport { Header } from \"./header\";\nimport { Sidebar } from \"./sidebar\";\n\ninterface LayoutProps {\n  children: React.ReactNode;\n}\n\nexport function Layout({ children }: LayoutProps) {\n  const [sidebarOpen, setSidebarOpen] = useState(true);\n\n  return (\n    <div className=\"h-screen flex bg-background\">\n      <Sidebar isOpen={sidebarOpen} />\n\n      <div className=\"flex-1 flex flex-col overflow-hidden\">\n        <Header\n          isSidebarOpen={sidebarOpen}\n          onMenuToggle={() => setSidebarOpen(!sidebarOpen)}\n        />\n\n        <main className=\"flex-1 overflow-auto\">\n          <div className=\"h-full\">{children}</div>\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/layout/sidebar-nav.tsx",
    "content": "import type { FileRoutesByTo } from \"@/lib/routeTree.gen\";\nimport { Link } from \"@tanstack/react-router\";\nimport type { LucideIcon } from \"lucide-react\";\n\ninterface SidebarNavItem {\n  icon: LucideIcon;\n  label: string;\n  to: keyof FileRoutesByTo;\n}\n\ninterface SidebarNavProps {\n  items: readonly SidebarNavItem[];\n}\n\nexport function SidebarNav({ items }: SidebarNavProps) {\n  return (\n    <nav className=\"flex-1 p-4 space-y-1\">\n      {items.map((item) => (\n        <Link\n          key={item.to}\n          to={item.to}\n          className=\"flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors\"\n          activeProps={{\n            className: \"bg-accent text-accent-foreground\",\n          }}\n        >\n          <item.icon className=\"h-4 w-4\" />\n          <span>{item.label}</span>\n        </Link>\n      ))}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/layout/sidebar.tsx",
    "content": "import { UserMenu } from \"@/components/user-menu\";\nimport { sidebarItems } from \"./constants\";\nimport { SidebarNav } from \"./sidebar-nav\";\n\ninterface SidebarProps {\n  isOpen: boolean;\n}\n\nexport function Sidebar({ isOpen }: SidebarProps) {\n  return (\n    <aside\n      className={`${\n        isOpen ? \"w-64\" : \"w-0\"\n      } transition-all duration-300 ease-in-out bg-muted/50 border-r overflow-hidden`}\n    >\n      <div className=\"h-full flex flex-col\">\n        <div className=\"h-14 flex items-center px-4 border-b\">\n          <h2 className=\"font-semibold text-lg\">Console</h2>\n        </div>\n        <SidebarNav items={sidebarItems} />\n        <UserMenu />\n      </div>\n    </aside>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/not-found.tsx",
    "content": "import { Button } from \"@repo/ui\";\nimport { Link } from \"@tanstack/react-router\";\n\nexport function NotFound() {\n  return (\n    <div className=\"flex min-h-svh flex-col items-center justify-center p-6\">\n      <div className=\"mx-auto max-w-md text-center\">\n        <h1 className=\"mb-2 text-4xl font-bold\">404</h1>\n        <p className=\"mb-6 text-muted-foreground\">\n          The page you're looking for doesn't exist.\n        </p>\n        <Button asChild>\n          <Link to=\"/\">Go Home</Link>\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/components/user-menu.tsx",
    "content": "import { signOut, useSessionQuery } from \"@/lib/queries/session\";\nimport { Avatar, AvatarFallback, Button } from \"@repo/ui\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { LogOut, RefreshCw, User } from \"lucide-react\";\n\n/** Displays current authenticated user and sign-out control. */\nexport function UserMenu() {\n  const queryClient = useQueryClient();\n  const { data: session, isPending, error, refetch } = useSessionQuery();\n\n  if (isPending) {\n    return (\n      <div className=\"flex items-center gap-2 px-3 py-2\">\n        <div className=\"h-8 w-8 rounded-full bg-muted animate-pulse\" />\n        <div className=\"flex-1\">\n          <div className=\"h-4 w-20 bg-muted rounded animate-pulse\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (error) {\n    return (\n      <div className=\"px-3 py-2 text-sm text-destructive\">\n        Failed to load session\n        <Button\n          variant=\"ghost\"\n          size=\"sm\"\n          onClick={() => refetch()}\n          className=\"ml-2\"\n        >\n          <RefreshCw className=\"h-3 w-3\" />\n          Retry\n        </Button>\n      </div>\n    );\n  }\n\n  const user = session?.user;\n\n  if (!user) {\n    return null;\n  }\n\n  return (\n    <div className=\"p-4 border-t\">\n      <div className=\"flex items-center gap-3 px-3 py-2\">\n        <Avatar className=\"h-8 w-8\">\n          <AvatarFallback>\n            {user.name?.[0]?.toUpperCase() || <User className=\"h-4 w-4\" />}\n          </AvatarFallback>\n        </Avatar>\n        <div className=\"flex-1 min-w-0\">\n          <p className=\"text-sm font-medium truncate\">{user.name || \"User\"}</p>\n          <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\n        </div>\n        <Button\n          variant=\"ghost\"\n          size=\"icon\"\n          onClick={() => signOut(queryClient)}\n          title=\"Sign out\"\n        >\n          <LogOut className=\"h-4 w-4\" />\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"css\": \"styles/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@repo/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "apps/app/global.d.ts",
    "content": "import * as React from \"react\";\nimport \"vite/client\";\n\ninterface Window {\n  dataLayer: unknown[];\n}\n\ninterface ImportMetaEnv {\n  readonly VITE_APP_NAME: string;\n  readonly VITE_APP_ORIGIN: string;\n  readonly VITE_GOOGLE_CLOUD_PROJECT: string;\n  readonly VITE_GA_MEASUREMENT_ID: string;\n}\n\ndeclare module \"relay-runtime\" {\n  interface PayloadError {\n    errors?: Record<string, string[] | undefined>;\n  }\n}\n\ndeclare module \"*.css\";\n\ndeclare module \"*.svg\" {\n  const content: React.FC<React.SVGProps<SVGElement>>;\n  export default content;\n}\n"
  },
  {
    "path": "apps/app/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>%VITE_APP_NAME%</title>\n    <meta\n      name=\"description\"\n      content=\"The web's most popular Jamstack React template\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <meta property=\"og:title\" content=\"\" />\n    <meta property=\"og:type\" content=\"\" />\n    <meta property=\"og:url\" content=\"\" />\n    <meta property=\"og:image\" content=\"\" />\n    <meta name=\"theme-color\" content=\"#fafafa\" />\n\n    <link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\" />\n    <link rel=\"apple-touch-icon\" href=\"/logo192.png\" />\n\n    <link rel=\"manifest\" href=\"/site.manifest\" />\n\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\"\n    />\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/app/index.tsx",
    "content": "import { QueryClientProvider } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { createRouter, RouterProvider } from \"@tanstack/react-router\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { NotFound } from \"./components/not-found\";\nimport { queryClient } from \"./lib/query\";\nimport { routeTree } from \"./lib/routeTree.gen\";\nimport \"./styles/globals.css\";\n\nconst router = createRouter({\n  routeTree,\n  context: { queryClient },\n  defaultPreload: \"intent\",\n  defaultNotFoundComponent: NotFound,\n});\n\nconst container = document.getElementById(\"root\");\nconst root = createRoot(container!);\n\nroot.render(\n  <StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <RouterProvider router={router} />\n      {import.meta.env.DEV && (\n        <ReactQueryDevtools\n          initialIsOpen={false}\n          buttonPosition=\"bottom-right\"\n        />\n      )}\n    </QueryClientProvider>\n  </StrictMode>,\n);\n\nif (import.meta.hot) {\n  import.meta.hot.dispose(() => root.unmount());\n}\n\ndeclare module \"@tanstack/react-router\" {\n  interface Register {\n    router: typeof router;\n  }\n}\n"
  },
  {
    "path": "apps/app/lib/auth-config.ts",
    "content": "// All durations in milliseconds. Providers must match server-side config.\n// Changing api.basePath requires updating server routing.\nexport const authConfig = {\n  oauth: {\n    providers: [\"google\"] as const,\n  },\n\n  passkey: {\n    enableConditionalUI: true,\n    timeout: 60_000,\n    userVerification: \"preferred\" as const,\n  },\n\n  security: {\n    csrfTokenHeader: \"x-csrf-token\",\n    sessionCookieName: \"better-auth.session\",\n  },\n\n  api: {\n    basePath: \"/api/auth\",\n    requestTimeout: import.meta.env.DEV ? 60_000 : 30_000,\n  },\n\n  retry: {\n    attempts: 3,\n    initialDelay: 1000,\n    maxDelay: 5000,\n    backoffMultiplier: 2,\n  },\n\n  session: {\n    checkInterval: 5 * 60 * 1000,\n    refreshThreshold: 10 * 60 * 1000,\n  },\n\n  errors: {\n    sessionExpired: \"Your session has expired. Please sign in again.\",\n    unauthorized: \"You need to sign in to access this page.\",\n    networkError: \"Network error. Please check your connection and try again.\",\n    passkeyNotSupported: \"Your browser doesn't support passkeys.\",\n    passkeyNotFound:\n      \"No passkey found for this account. Please sign in with Google first.\",\n    genericError: \"Something went wrong. Please try again.\",\n  },\n} as const;\n\n// Only allows same-origin relative paths (starts with \"/\" but not \"//\")\nexport function isValidRedirectUrl(url: string): boolean {\n  return url.startsWith(\"/\") && !url.startsWith(\"//\");\n}\n\n// Returns \"/\" for invalid or missing URLs\nexport function getSafeRedirectUrl(url: unknown): string {\n  if (typeof url !== \"string\" || !url) {\n    return \"/\";\n  }\n\n  return isValidRedirectUrl(url) ? url : \"/\";\n}\n\n// Refresh when expiry is within threshold to prevent mid-operation failures.\n// Returns false for already-expired sessions.\nexport function shouldRefreshSession(\n  expiresAt: Date | string | undefined,\n): boolean {\n  if (!expiresAt) return false;\n\n  const expiryTime =\n    typeof expiresAt === \"string\"\n      ? new Date(expiresAt).getTime()\n      : expiresAt.getTime();\n\n  const now = Date.now();\n  const timeUntilExpiry = expiryTime - now;\n\n  return (\n    timeUntilExpiry > 0 && timeUntilExpiry < authConfig.session.refreshThreshold\n  );\n}\n"
  },
  {
    "path": "apps/app/lib/auth.ts",
    "content": "/**\n * @file Better Auth client instance.\n *\n * Do not use auth.useSession() directly - use TanStack Query wrappers\n * from lib/queries/session.ts to ensure proper caching and consistency.\n */\n\nimport { passkeyClient } from \"@better-auth/passkey/client\";\nimport { stripeClient } from \"@better-auth/stripe/client\";\nimport {\n  anonymousClient,\n  emailOTPClient,\n  organizationClient,\n} from \"better-auth/client/plugins\";\nimport { createAuthClient } from \"better-auth/react\";\nimport { authConfig } from \"./auth-config\";\n\nconst baseURL =\n  typeof window !== \"undefined\"\n    ? window.location.origin\n    : \"http://localhost:5173\";\n\nexport const auth = createAuthClient({\n  baseURL: baseURL + authConfig.api.basePath,\n  plugins: [\n    anonymousClient(),\n    emailOTPClient(),\n    organizationClient(),\n    passkeyClient(),\n    stripeClient({ subscription: true }),\n  ],\n});\n\nexport type AuthClient = typeof auth;\n\n// Inferred types from configured instance - includes plugin extensions\n// $Infer.Session is the full response shape { user, session }\ntype SessionResponse = typeof auth.$Infer.Session;\nexport type User = SessionResponse[\"user\"];\nexport type Session = SessionResponse[\"session\"];\n"
  },
  {
    "path": "apps/app/lib/errors.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  getErrorMessage,\n  getErrorStatus,\n  isUnauthenticatedError,\n} from \"./errors\";\n\ndescribe(\"getErrorStatus\", () => {\n  it(\"returns undefined for non-objects\", () => {\n    expect(getErrorStatus(null)).toBeUndefined();\n    expect(getErrorStatus(undefined)).toBeUndefined();\n    expect(getErrorStatus(\"string\")).toBeUndefined();\n    expect(getErrorStatus(123)).toBeUndefined();\n  });\n\n  it(\"extracts direct status property\", () => {\n    expect(getErrorStatus({ status: 401 })).toBe(401);\n    expect(getErrorStatus({ status: 500 })).toBe(500);\n  });\n\n  it(\"ignores non-numeric status\", () => {\n    expect(getErrorStatus({ status: \"401\" })).toBeUndefined();\n    expect(getErrorStatus({ status: null })).toBeUndefined();\n  });\n\n  it(\"extracts nested response.status (axios-style)\", () => {\n    expect(getErrorStatus({ response: { status: 403 } })).toBe(403);\n  });\n\n  it(\"follows error cause chain\", () => {\n    const nested = { status: 401 };\n    const wrapper = { cause: nested };\n    expect(getErrorStatus(wrapper)).toBe(401);\n  });\n\n  it(\"handles deep cause chains\", () => {\n    const deep = { cause: { cause: { cause: { status: 500 } } } };\n    expect(getErrorStatus(deep)).toBe(500);\n  });\n\n  it(\"handles circular cause references without stack overflow\", () => {\n    const circular: Record<string, unknown> = { status: undefined };\n    circular.cause = circular;\n    expect(getErrorStatus(circular)).toBeUndefined();\n  });\n\n  it(\"prefers direct status over nested\", () => {\n    expect(getErrorStatus({ status: 401, response: { status: 500 } })).toBe(\n      401,\n    );\n  });\n});\n\ndescribe(\"getErrorMessage\", () => {\n  it(\"extracts message from Error instances\", () => {\n    expect(getErrorMessage(new Error(\"Something broke\"))).toBe(\n      \"Something broke\",\n    );\n  });\n\n  it(\"returns string errors directly\", () => {\n    expect(getErrorMessage(\"Direct error message\")).toBe(\n      \"Direct error message\",\n    );\n  });\n\n  it(\"extracts statusText from Response-like objects\", () => {\n    expect(getErrorMessage({ statusText: \"Not Found\" })).toBe(\"Not Found\");\n  });\n\n  it(\"returns fallback for unknown error shapes\", () => {\n    expect(getErrorMessage(null)).toBe(\"An unexpected error occurred\");\n    expect(getErrorMessage(undefined)).toBe(\"An unexpected error occurred\");\n    expect(getErrorMessage({})).toBe(\"An unexpected error occurred\");\n    expect(getErrorMessage({ statusText: \"\" })).toBe(\n      \"An unexpected error occurred\",\n    );\n  });\n});\n\ndescribe(\"isUnauthenticatedError\", () => {\n  it(\"returns true for 401 status\", () => {\n    expect(isUnauthenticatedError({ status: 401 })).toBe(true);\n  });\n\n  it(\"returns false for 403 status (authorization, not authentication)\", () => {\n    expect(isUnauthenticatedError({ status: 403 })).toBe(false);\n  });\n\n  it(\"returns false for other status codes\", () => {\n    expect(isUnauthenticatedError({ status: 500 })).toBe(false);\n    expect(isUnauthenticatedError({ status: 404 })).toBe(false);\n  });\n\n  it(\"returns false for non-error values\", () => {\n    expect(isUnauthenticatedError(null)).toBe(false);\n    expect(isUnauthenticatedError(\"error\")).toBe(false);\n    expect(isUnauthenticatedError({})).toBe(false);\n  });\n\n  it(\"detects 401 in nested cause\", () => {\n    expect(isUnauthenticatedError({ cause: { status: 401 } })).toBe(true);\n  });\n\n  it(\"returns true for tRPC UNAUTHORIZED code\", () => {\n    expect(isUnauthenticatedError({ data: { code: \"UNAUTHORIZED\" } })).toBe(\n      true,\n    );\n  });\n\n  it(\"returns false for tRPC FORBIDDEN code\", () => {\n    expect(isUnauthenticatedError({ data: { code: \"FORBIDDEN\" } })).toBe(false);\n  });\n});\n"
  },
  {
    "path": "apps/app/lib/errors.ts",
    "content": "// Extract HTTP status from various error shapes (with cycle guard)\nexport function getErrorStatus(\n  error: unknown,\n  seen = new WeakSet<object>(),\n): number | undefined {\n  if (!error || typeof error !== \"object\") return undefined;\n  if (seen.has(error)) return undefined;\n  seen.add(error);\n\n  const err = error as Record<string, unknown>;\n  // Direct status (tRPC, Better Auth)\n  if (typeof err.status === \"number\") return err.status;\n  // Nested in response (axios-style)\n  if (\n    err.response &&\n    typeof (err.response as Record<string, unknown>).status === \"number\"\n  ) {\n    return (err.response as Record<string, unknown>).status as number;\n  }\n  // Error cause chain\n  if (err.cause) return getErrorStatus(err.cause, seen);\n  return undefined;\n}\n\n// Check if error indicates unauthenticated state (401).\n// Maps tRPC UNAUTHORIZED code and HTTP 401 status to a semantic boolean.\n// Does not match 403 (forbidden) - that means authenticated but lacking permission.\nexport function isUnauthenticatedError(error: unknown): boolean {\n  // tRPC errors expose typed code\n  if (error && typeof error === \"object\" && \"data\" in error) {\n    const data = (error as { data?: { code?: string } }).data;\n    if (data?.code === \"UNAUTHORIZED\") return true;\n  }\n  return getErrorStatus(error) === 401;\n}\n\n// Safely extract message from any thrown value\nexport function getErrorMessage(error: unknown): string {\n  if (error instanceof Error) return error.message;\n  if (typeof error === \"string\") return error;\n  // Response objects (fetch API)\n  if (error && typeof error === \"object\" && \"statusText\" in error) {\n    const statusText = (error as { statusText?: string }).statusText;\n    if (statusText) return statusText;\n  }\n  return \"An unexpected error occurred\";\n}\n"
  },
  {
    "path": "apps/app/lib/queries/README.md",
    "content": "# TanStack Queries\n\nThis folder contains TanStack Query implementations for managing server state and data fetching in the application.\n\n## Overview\n\nTanStack Query provides powerful asynchronous state management for TypeScript/JavaScript applications. All query definitions in this folder follow a consistent pattern that enables:\n\n- **Automatic caching** - Data is cached and reused across components\n- **Background refetching** - Stale data is automatically refreshed\n- **Request deduplication** - Multiple components requesting the same data result in a single network request\n- **Optimistic updates** - UI updates immediately while mutations are in-flight\n- **Smart refetching** - Automatic refetch on window focus, network reconnect, and at configurable intervals\n\n## File Structure\n\nEach query module typically exports:\n\n1. **Query Keys** - Unique identifiers for cache entries\n2. **Query Options** - Factory functions returning query configurations\n3. **Custom Hooks** - React hooks for consuming queries\n4. **Utility Functions** - Helpers for prefetching, invalidation, and manual updates\n\n## Pattern Example\n\n```typescript\n// user.ts - Example query module structure\n\nimport {\n  queryOptions,\n  useQuery,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport type { QueryClient } from \"@tanstack/react-query\";\n\n// 1. Define query keys with consistent naming\nexport const userQueryKey = [\"users\", \"detail\"] as const;\nexport const usersListQueryKey = [\"users\", \"list\"] as const;\n\n// 2. Create query options factory functions\nexport function userQueryOptions(userId: string) {\n  return queryOptions({\n    queryKey: [...userQueryKey, userId],\n    queryFn: async () => {\n      const response = await fetch(`/api/users/${userId}`);\n      if (!response.ok) throw new Error(\"Failed to fetch user\");\n      return response.json();\n    },\n    staleTime: 5 * 60_000, // Consider data fresh for 5 minutes\n    gcTime: 10 * 60_000, // Keep in cache for 10 minutes\n  });\n}\n\n// 3. Export convenient hooks\nexport function useUser(userId: string) {\n  return useQuery(userQueryOptions(userId));\n}\n\nexport function useSuspenseUser(userId: string) {\n  return useSuspenseQuery(userQueryOptions(userId));\n}\n\n// 4. Provide utility functions\nexport async function prefetchUser(queryClient: QueryClient, userId: string) {\n  return queryClient.prefetchQuery(userQueryOptions(userId));\n}\n\nexport function invalidateUser(queryClient: QueryClient, userId: string) {\n  return queryClient.invalidateQueries({\n    queryKey: [...userQueryKey, userId],\n  });\n}\n```\n\n## Query Key Conventions\n\nQuery keys should follow a hierarchical structure:\n\n```typescript\n[\"resource\"][(\"resource\", \"list\")][(\"resource\", \"list\", { filters })][ // All queries for a resource // List queries // List with filters\n  (\"resource\", \"detail\", id)\n][(\"resource\", \"detail\", id, \"related\")]; // Single item queries // Nested resources\n```\n\n## Configuration Guidelines\n\n### staleTime\n\n- How long data is considered fresh\n- During this time, no background refetches occur\n- Set based on data volatility (user sessions: 30s, static content: hours)\n\n### gcTime (garbage collection time)\n\n- How long to keep unused data in cache\n- Should be >= staleTime\n- Prevents refetching when navigating back quickly\n\n### refetchOnWindowFocus\n\n- Ensures data is fresh when users return\n- Disable for rarely-changing data\n- Critical for authentication state\n\n### retry\n\n- Number of retry attempts for failed queries\n- Use exponential backoff with retryDelay\n- Consider disabling for 4xx errors\n\n## Current Implementations\n\n### `session.ts`\n\nManages authentication session state with Better Auth integration:\n\n- Automatic session refresh before expiry\n- Optimistic updates during auth state changes\n- Cache invalidation on login/logout\n- Prefetching for protected routes\n\n## Best Practices\n\n1. **Colocate queries with their domain** - Keep related queries in the same file\n2. **Export query options** - Allows usage in loaders and prefetching\n3. **Use TypeScript** - Define return types for better type safety\n4. **Handle errors gracefully** - Queries should throw meaningful errors\n5. **Optimize cache times** - Balance freshness with performance\n6. **Leverage suspense** - Use `useSuspenseQuery` with error boundaries\n7. **Prefetch critical data** - Load data before users need it\n\n## Testing\n\nWhen testing components that use queries:\n\n```typescript\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\n\nconst createTestQueryClient = () => new QueryClient({\n  defaultOptions: {\n    queries: { retry: false, staleTime: Infinity },\n    mutations: { retry: false },\n  },\n});\n\n// In your test\nconst queryClient = createTestQueryClient();\nrender(\n  <QueryClientProvider client={queryClient}>\n    <YourComponent />\n  </QueryClientProvider>\n);\n```\n\n## Resources\n\n- [TanStack Query Documentation](https://tanstack.com/query/latest)\n- [Query Keys](https://tanstack.com/query/latest/docs/framework/react/guides/query-keys)\n- [Query Functions](https://tanstack.com/query/latest/docs/framework/react/guides/query-functions)\n- [Suspense](https://tanstack.com/query/latest/docs/framework/react/guides/suspense)\n"
  },
  {
    "path": "apps/app/lib/queries/billing.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { billingQueryKey, billingQueryOptions } from \"./billing\";\n\ndescribe(\"billingQueryOptions\", () => {\n  it(\"includes activeOrgId in query key\", () => {\n    const { queryKey } = billingQueryOptions(\"org-123\");\n    expect(queryKey).toEqual([\"billing\", \"subscription\", \"org-123\"]);\n  });\n\n  it(\"normalizes undefined to null in query key\", () => {\n    const { queryKey } = billingQueryOptions(undefined);\n    expect(queryKey).toEqual([\"billing\", \"subscription\", null]);\n  });\n\n  it(\"normalizes missing arg to null in query key\", () => {\n    const { queryKey } = billingQueryOptions();\n    expect(queryKey).toEqual([\"billing\", \"subscription\", null]);\n  });\n\n  it(\"preserves explicit null in query key\", () => {\n    const { queryKey } = billingQueryOptions(null);\n    expect(queryKey).toEqual([\"billing\", \"subscription\", null]);\n  });\n\n  it(\"produces distinct keys for different orgs\", () => {\n    expect(billingQueryOptions(\"org-1\").queryKey).not.toEqual(\n      billingQueryOptions(\"org-2\").queryKey,\n    );\n  });\n});\n\ndescribe(\"billingQueryKey\", () => {\n  it(\"is a prefix of the full query key for bulk invalidation\", () => {\n    const { queryKey } = billingQueryOptions(\"org-1\");\n    expect(queryKey.slice(0, billingQueryKey.length)).toEqual([\n      ...billingQueryKey,\n    ]);\n  });\n});\n"
  },
  {
    "path": "apps/app/lib/queries/billing.ts",
    "content": "// Billing subscription state via TanStack Query.\n// Query key includes activeOrgId so switching organizations refetches automatically.\n\nimport {\n  queryOptions,\n  useQuery,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport { trpcClient } from \"../trpc\";\n\n// Partial key for bulk invalidation (e.g. after subscription change)\nexport const billingQueryKey = [\"billing\", \"subscription\"] as const;\n\nexport function billingQueryOptions(activeOrgId?: string | null) {\n  return queryOptions({\n    queryKey: [...billingQueryKey, activeOrgId ?? null] as const,\n    queryFn: () => trpcClient.billing.subscription.query(),\n  });\n}\n\nexport function useBillingQuery(activeOrgId?: string | null) {\n  return useQuery(billingQueryOptions(activeOrgId));\n}\n\nexport function useSuspenseBillingQuery(activeOrgId?: string | null) {\n  return useSuspenseQuery(billingQueryOptions(activeOrgId));\n}\n"
  },
  {
    "path": "apps/app/lib/queries/session.test.ts",
    "content": "import { QueryClient } from \"@tanstack/react-query\";\nimport { describe, expect, it } from \"vitest\";\nimport { getCachedSession, isAuthenticated, sessionQueryKey } from \"./session\";\n\nfunction createQueryClient() {\n  return new QueryClient({\n    defaultOptions: { queries: { retry: false } },\n  });\n}\n\ndescribe(\"isAuthenticated\", () => {\n  it(\"returns false when no session data cached\", () => {\n    const queryClient = createQueryClient();\n    expect(isAuthenticated(queryClient)).toBe(false);\n  });\n\n  it(\"returns false when session is null\", () => {\n    const queryClient = createQueryClient();\n    queryClient.setQueryData(sessionQueryKey, null);\n    expect(isAuthenticated(queryClient)).toBe(false);\n  });\n\n  it(\"returns false when user is missing\", () => {\n    const queryClient = createQueryClient();\n    queryClient.setQueryData(sessionQueryKey, {\n      session: { id: \"1\" },\n      user: null,\n    });\n    expect(isAuthenticated(queryClient)).toBe(false);\n  });\n\n  it(\"returns false when session is missing\", () => {\n    const queryClient = createQueryClient();\n    queryClient.setQueryData(sessionQueryKey, {\n      user: { id: \"1\" },\n      session: null,\n    });\n    expect(isAuthenticated(queryClient)).toBe(false);\n  });\n\n  it(\"returns true when both user and session exist\", () => {\n    const queryClient = createQueryClient();\n    queryClient.setQueryData(sessionQueryKey, {\n      user: { id: \"user-1\", email: \"test@example.com\" },\n      session: { id: \"session-1\", expiresAt: new Date() },\n    });\n    expect(isAuthenticated(queryClient)).toBe(true);\n  });\n});\n\ndescribe(\"getCachedSession\", () => {\n  it(\"returns undefined when no data cached\", () => {\n    const queryClient = createQueryClient();\n    expect(getCachedSession(queryClient)).toBeUndefined();\n  });\n\n  it(\"returns cached session data\", () => {\n    const queryClient = createQueryClient();\n    const sessionData = {\n      user: { id: \"user-1\" },\n      session: { id: \"session-1\" },\n    };\n    queryClient.setQueryData(sessionQueryKey, sessionData);\n    expect(getCachedSession(queryClient)).toEqual(sessionData);\n  });\n});\n"
  },
  {
    "path": "apps/app/lib/queries/session.ts",
    "content": "/**\n * @file Session state managed exclusively via TanStack Query.\n *\n * Do not use direct auth.getSession() calls or local storage for sessions.\n * TanStack Query handles caching, refresh, and consistency automatically.\n */\n\nimport { getErrorStatus } from \"@/lib/errors\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport {\n  queryOptions,\n  useQuery,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport { auth, type Session, type User } from \"../auth\";\n\n// Both user and session must be present for valid auth state\nexport interface SessionData {\n  user: User;\n  session: Session;\n}\n\nexport const sessionQueryKey = [\"auth\", \"session\"] as const;\n\n// Returns null when unauthenticated (not an error condition).\n// Only overrides staleTime and retry — inherits gcTime, refetchOnWindowFocus,\n// refetchOnReconnect, and retryDelay from QueryClient defaults.\nexport function sessionQueryOptions() {\n  return queryOptions<SessionData | null>({\n    queryKey: sessionQueryKey,\n    queryFn: async () => {\n      const response = await auth.getSession();\n      if (response.error) {\n        throw response.error;\n      }\n      return response.data;\n    },\n    // Shorter freshness than global 2min — auth state should stay current\n    staleTime: 30_000,\n    // Don't retry 401/403 — retrying won't help for auth/permission errors\n    retry(failureCount, error) {\n      const status = getErrorStatus(error);\n      if (status === 401 || status === 403) return false;\n      return failureCount < 3;\n    },\n  });\n}\n\nexport function useSessionQuery() {\n  return useQuery(sessionQueryOptions());\n}\n\nexport function useSuspenseSessionQuery() {\n  return useSuspenseQuery(sessionQueryOptions());\n}\n\nexport function getCachedSession(\n  queryClient: QueryClient,\n): SessionData | null | undefined {\n  return queryClient.getQueryData(sessionQueryKey);\n}\n\n// Sync check of cached data only - does not trigger network request.\n// Both user AND session must exist to handle partial data edge cases.\nexport function isAuthenticated(queryClient: QueryClient): boolean {\n  const session = getCachedSession(queryClient);\n  return session?.user != null && session?.session != null;\n}\n\n// Clears server session, then updates cache and redirects.\n// Uses setQueryData(null) instead of invalidateQueries to avoid a wasted\n// refetch — session is binary state, not partially stale data.\n// Hard redirect resets all in-memory state (Jotai atoms, component state)\n// for a clean slate between user sessions.\nexport async function signOut(\n  queryClient: QueryClient,\n  options?: { redirect?: boolean },\n) {\n  try {\n    await auth.signOut();\n  } finally {\n    queryClient.setQueryData(sessionQueryKey, null);\n\n    if (options?.redirect !== false) {\n      window.location.href = \"/login\";\n    }\n  }\n}\n\n/**\n * Clears session cache and revalidates router after auth state changes.\n * Uses removeQueries (not invalidate) so beforeLoad sees undefined and fetches fresh.\n */\nexport async function revalidateSession(\n  queryClient: QueryClient,\n  router: { invalidate: () => Promise<void> },\n) {\n  queryClient.removeQueries({ queryKey: sessionQueryKey });\n  await router.invalidate();\n}\n"
  },
  {
    "path": "apps/app/lib/query.ts",
    "content": "import { QueryClient } from \"@tanstack/react-query\";\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      // Data remains fresh for 2 minutes - prevents redundant API calls during\n      // typical user sessions while ensuring data updates within reasonable time\n      staleTime: 2 * 60 * 1000,\n      // Garbage collection after 5 minutes - balances memory usage with instant\n      // data availability when navigating back to recently viewed pages\n      gcTime: 5 * 60 * 1000,\n      // Retry strategy: 3 attempts with exponential backoff (1s, 2s, 4s) capped at 30s\n      // Handles transient network issues without overwhelming the server\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      // Auto-refetch when user returns to tab - ensures displayed data is current\n      // after context switches (critical for collaborative features)\n      refetchOnWindowFocus: true,\n      // Always refetch after network reconnection - prevents stale data after\n      // connectivity issues (overrides staleTime check)\n      refetchOnReconnect: \"always\",\n    },\n    mutations: {\n      // Single retry for mutations - prevents duplicate operations while handling\n      // momentary network blips (user can manually retry for persistent failures)\n      retry: 1,\n      retryDelay: 1000,\n      onError: (error) => console.error(\"Mutation failed:\", error),\n    },\n  },\n});\n"
  },
  {
    "path": "apps/app/lib/routeTree.gen.ts",
    "content": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './../routes/__root'\nimport { Route as appRouteRouteImport } from './../routes/(app)/route'\nimport { Route as appIndexRouteImport } from './../routes/(app)/index'\nimport { Route as authSignupRouteImport } from './../routes/(auth)/signup'\nimport { Route as authLoginRouteImport } from './../routes/(auth)/login'\nimport { Route as appUsersRouteImport } from './../routes/(app)/users'\nimport { Route as appSettingsRouteImport } from './../routes/(app)/settings'\nimport { Route as appReportsRouteImport } from './../routes/(app)/reports'\nimport { Route as appDashboardRouteImport } from './../routes/(app)/dashboard'\nimport { Route as appAnalyticsRouteImport } from './../routes/(app)/analytics'\nimport { Route as appAboutRouteImport } from './../routes/(app)/about'\n\nconst appRouteRoute = appRouteRouteImport.update({\n  id: '/(app)',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst appIndexRoute = appIndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst authSignupRoute = authSignupRouteImport.update({\n  id: '/(auth)/signup',\n  path: '/signup',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst authLoginRoute = authLoginRouteImport.update({\n  id: '/(auth)/login',\n  path: '/login',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst appUsersRoute = appUsersRouteImport.update({\n  id: '/users',\n  path: '/users',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst appSettingsRoute = appSettingsRouteImport.update({\n  id: '/settings',\n  path: '/settings',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst appReportsRoute = appReportsRouteImport.update({\n  id: '/reports',\n  path: '/reports',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst appDashboardRoute = appDashboardRouteImport.update({\n  id: '/dashboard',\n  path: '/dashboard',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst appAnalyticsRoute = appAnalyticsRouteImport.update({\n  id: '/analytics',\n  path: '/analytics',\n  getParentRoute: () => appRouteRoute,\n} as any)\nconst appAboutRoute = appAboutRouteImport.update({\n  id: '/about',\n  path: '/about',\n  getParentRoute: () => appRouteRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n  '/about': typeof appAboutRoute\n  '/analytics': typeof appAnalyticsRoute\n  '/dashboard': typeof appDashboardRoute\n  '/reports': typeof appReportsRoute\n  '/settings': typeof appSettingsRoute\n  '/users': typeof appUsersRoute\n  '/login': typeof authLoginRoute\n  '/signup': typeof authSignupRoute\n  '/': typeof appIndexRoute\n}\nexport interface FileRoutesByTo {\n  '/about': typeof appAboutRoute\n  '/analytics': typeof appAnalyticsRoute\n  '/dashboard': typeof appDashboardRoute\n  '/reports': typeof appReportsRoute\n  '/settings': typeof appSettingsRoute\n  '/users': typeof appUsersRoute\n  '/login': typeof authLoginRoute\n  '/signup': typeof authSignupRoute\n  '/': typeof appIndexRoute\n}\nexport interface FileRoutesById {\n  __root__: typeof rootRouteImport\n  '/(app)': typeof appRouteRouteWithChildren\n  '/(app)/about': typeof appAboutRoute\n  '/(app)/analytics': typeof appAnalyticsRoute\n  '/(app)/dashboard': typeof appDashboardRoute\n  '/(app)/reports': typeof appReportsRoute\n  '/(app)/settings': typeof appSettingsRoute\n  '/(app)/users': typeof appUsersRoute\n  '/(auth)/login': typeof authLoginRoute\n  '/(auth)/signup': typeof authSignupRoute\n  '/(app)/': typeof appIndexRoute\n}\nexport interface FileRouteTypes {\n  fileRoutesByFullPath: FileRoutesByFullPath\n  fullPaths:\n    | '/about'\n    | '/analytics'\n    | '/dashboard'\n    | '/reports'\n    | '/settings'\n    | '/users'\n    | '/login'\n    | '/signup'\n    | '/'\n  fileRoutesByTo: FileRoutesByTo\n  to:\n    | '/about'\n    | '/analytics'\n    | '/dashboard'\n    | '/reports'\n    | '/settings'\n    | '/users'\n    | '/login'\n    | '/signup'\n    | '/'\n  id:\n    | '__root__'\n    | '/(app)'\n    | '/(app)/about'\n    | '/(app)/analytics'\n    | '/(app)/dashboard'\n    | '/(app)/reports'\n    | '/(app)/settings'\n    | '/(app)/users'\n    | '/(auth)/login'\n    | '/(auth)/signup'\n    | '/(app)/'\n  fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n  appRouteRoute: typeof appRouteRouteWithChildren\n  authLoginRoute: typeof authLoginRoute\n  authSignupRoute: typeof authSignupRoute\n}\n\ndeclare module '@tanstack/react-router' {\n  interface FileRoutesByPath {\n    '/(app)': {\n      id: '/(app)'\n      path: ''\n      fullPath: ''\n      preLoaderRoute: typeof appRouteRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(app)/': {\n      id: '/(app)/'\n      path: '/'\n      fullPath: '/'\n      preLoaderRoute: typeof appIndexRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(auth)/signup': {\n      id: '/(auth)/signup'\n      path: '/signup'\n      fullPath: '/signup'\n      preLoaderRoute: typeof authSignupRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(auth)/login': {\n      id: '/(auth)/login'\n      path: '/login'\n      fullPath: '/login'\n      preLoaderRoute: typeof authLoginRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/(app)/users': {\n      id: '/(app)/users'\n      path: '/users'\n      fullPath: '/users'\n      preLoaderRoute: typeof appUsersRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(app)/settings': {\n      id: '/(app)/settings'\n      path: '/settings'\n      fullPath: '/settings'\n      preLoaderRoute: typeof appSettingsRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(app)/reports': {\n      id: '/(app)/reports'\n      path: '/reports'\n      fullPath: '/reports'\n      preLoaderRoute: typeof appReportsRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(app)/dashboard': {\n      id: '/(app)/dashboard'\n      path: '/dashboard'\n      fullPath: '/dashboard'\n      preLoaderRoute: typeof appDashboardRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(app)/analytics': {\n      id: '/(app)/analytics'\n      path: '/analytics'\n      fullPath: '/analytics'\n      preLoaderRoute: typeof appAnalyticsRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n    '/(app)/about': {\n      id: '/(app)/about'\n      path: '/about'\n      fullPath: '/about'\n      preLoaderRoute: typeof appAboutRouteImport\n      parentRoute: typeof appRouteRoute\n    }\n  }\n}\n\ninterface appRouteRouteChildren {\n  appAboutRoute: typeof appAboutRoute\n  appAnalyticsRoute: typeof appAnalyticsRoute\n  appDashboardRoute: typeof appDashboardRoute\n  appReportsRoute: typeof appReportsRoute\n  appSettingsRoute: typeof appSettingsRoute\n  appUsersRoute: typeof appUsersRoute\n  appIndexRoute: typeof appIndexRoute\n}\n\nconst appRouteRouteChildren: appRouteRouteChildren = {\n  appAboutRoute: appAboutRoute,\n  appAnalyticsRoute: appAnalyticsRoute,\n  appDashboardRoute: appDashboardRoute,\n  appReportsRoute: appReportsRoute,\n  appSettingsRoute: appSettingsRoute,\n  appUsersRoute: appUsersRoute,\n  appIndexRoute: appIndexRoute,\n}\n\nconst appRouteRouteWithChildren = appRouteRoute._addFileChildren(\n  appRouteRouteChildren,\n)\n\nconst rootRouteChildren: RootRouteChildren = {\n  appRouteRoute: appRouteRouteWithChildren,\n  authLoginRoute: authLoginRoute,\n  authSignupRoute: authSignupRoute,\n}\nexport const routeTree = rootRouteImport\n  ._addFileChildren(rootRouteChildren)\n  ._addFileTypes<FileRouteTypes>()\n"
  },
  {
    "path": "apps/app/lib/store.ts",
    "content": "import { createStore, Provider } from \"jotai\";\nimport type { ReactNode } from \"react\";\nimport { createElement } from \"react\";\n\n/**\n * Global state management powered by Jotai.\n * @see https://jotai.org/\n */\nexport const store = createStore();\n\nexport function StoreProvider(props: StoreProviderProps) {\n  return createElement(Provider, { store, ...props });\n}\n\nexport type StoreProviderProps = {\n  children: ReactNode;\n};\n"
  },
  {
    "path": "apps/app/lib/trpc.ts",
    "content": "import type { AppRouter } from \"@repo/api\";\nimport {\n  createTRPCClient,\n  httpBatchLink,\n  type TRPCLink,\n  loggerLink,\n} from \"@trpc/client\";\nimport { createTRPCOptionsProxy } from \"@trpc/tanstack-react-query\";\nimport { queryClient } from \"./query\";\n\n// Build links array conditionally based on environment\nconst links: TRPCLink<AppRouter>[] = [];\n\n// Add logger link in development for debugging\nif (import.meta.env.DEV) {\n  links.push(\n    loggerLink({\n      enabled: (opts) =>\n        (import.meta.env.DEV && typeof window !== \"undefined\") ||\n        (opts.direction === \"down\" && opts.result instanceof Error),\n    }),\n  );\n}\n\n// Add HTTP batch link for actual requests\nlinks.push(\n  httpBatchLink({\n    url: `${import.meta.env.VITE_API_URL || \"/api\"}/trpc`,\n    // Custom headers for request tracking\n    headers() {\n      return {\n        \"x-trpc-source\": \"react-app\",\n      };\n    },\n    // Include credentials for authentication\n    fetch(url, options) {\n      return fetch(url, {\n        ...options,\n        credentials: \"include\",\n      });\n    },\n  }),\n);\n\nexport const trpcClient = createTRPCClient<AppRouter>({ links });\n\nexport const api = createTRPCOptionsProxy<AppRouter>({\n  client: trpcClient,\n  queryClient,\n});\n"
  },
  {
    "path": "apps/app/lib/utils.ts",
    "content": "export { cn } from \"@repo/ui\";\n"
  },
  {
    "path": "apps/app/package.json",
    "content": "{\n  \"name\": \"@repo/app\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite serve\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"test\": \"vitest\",\n    \"coverage\": \"vitest --coverage\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"deploy\": \"wrangler deploy --env-file ../../.env --env-file ../../.env.local\",\n    \"logs\": \"wrangler tail --env-file ../../.env --env-file ../../.env.local\"\n  },\n  \"dependencies\": {\n    \"@better-auth/passkey\": \"^1.4.18\",\n    \"@better-auth/stripe\": \"^1.4.18\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\n    \"@radix-ui/react-radio-group\": \"^1.3.8\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"@repo/ui\": \"workspace:*\",\n    \"@tanstack/react-query\": \"^5.90.21\",\n    \"@tanstack/react-router\": \"^1.161.1\",\n    \"@trpc/client\": \"^11.10.0\",\n    \"@trpc/tanstack-react-query\": \"^11.10.0\",\n    \"better-auth\": \"^1.4.18\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"jotai\": \"^2.17.1\",\n    \"jotai-effect\": \"^2.2.3\",\n    \"localforage\": \"^1.10.0\",\n    \"lucide-react\": \"^0.574.0\",\n    \"react\": \"^19.2.4\",\n    \"react-dom\": \"^19.2.4\",\n    \"react-error-boundary\": \"^6.1.1\",\n    \"tailwind-merge\": \"^3.4.1\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.2.0\",\n    \"@tanstack/react-query-devtools\": \"^5.91.3\",\n    \"@tanstack/react-router-devtools\": \"^1.161.1\",\n    \"@tanstack/router-plugin\": \"^1.161.1\",\n    \"@testing-library/jest-dom\": \"^6.9.1\",\n    \"@testing-library/react\": \"^16.3.2\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@types/bun\": \"^1.3.9\",\n    \"@types/node\": \"^25.2.3\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@vitejs/plugin-react\": \"^5.1.4\",\n    \"@vitejs/plugin-react-swc\": \"^4.2.3\",\n    \"autoprefixer\": \"^10.4.24\",\n    \"envars\": \"^1.1.1\",\n    \"execa\": \"^9.6.1\",\n    \"globby\": \"^16.1.1\",\n    \"happy-dom\": \"^20.6.2\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.2.0\",\n    \"tw-animate-css\": \"^1.4.0\",\n    \"typescript\": \"~5.9.3\",\n    \"vite\": \"~7.3.1\",\n    \"vite-tsconfig-paths\": \"^6.1.1\",\n    \"vitest\": \"~4.0.18\",\n    \"wrangler\": \"^4.66.0\"\n  }\n}\n"
  },
  {
    "path": "apps/app/postcss.config.js",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/app/public/robots.txt",
    "content": "# www.robotstxt.org/\n\n# Allow crawling of all content\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/app/public/site.manifest",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \"/?utm_source=homescreen\",\n  \"display\": \"standalone\",\n  \"background_color\": \"#fafafa\",\n  \"theme_color\": \"#fafafa\"\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/about.tsx",
    "content": "import {\n  Button,\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n  Separator,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(app)/about\")({\n  component: About,\n});\n\nfunction About() {\n  return (\n    <div className=\"container mx-auto px-4 sm:px-6 lg:px-8 py-20\">\n      {/* Hero Section */}\n      <div className=\"text-center mb-16\">\n        <h1 className=\"text-4xl font-bold tracking-tight mb-6\">\n          About React Starter Kit\n        </h1>\n        <p className=\"text-xl text-muted-foreground max-w-3xl mx-auto\">\n          A production-ready, full-stack web application template that combines\n          modern development practices with cutting-edge technologies to deliver\n          exceptional performance and developer experience.\n        </p>\n      </div>\n\n      {/* Mission Section */}\n      <section className=\"mb-20\">\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"text-2xl\">Our Mission</CardTitle>\n            <CardDescription>\n              Empowering developers to build faster, better web applications\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <p className=\"text-muted-foreground\">\n              React Starter Kit was created to bridge the gap between prototype\n              and production. We believe that developers should focus on\n              building great features, not wrestling with configuration and\n              setup.\n            </p>\n            <p className=\"text-muted-foreground\">\n              Our template provides a solid foundation with best practices,\n              modern tooling, and optimized performance out of the box, so you\n              can ship your ideas faster and with confidence.\n            </p>\n          </CardContent>\n        </Card>\n      </section>\n\n      {/* Key Features */}\n      <section className=\"mb-20\">\n        <h2 className=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n          What Makes Us Different\n        </h2>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n          <Card>\n            <CardHeader>\n              <CardTitle>🎯 Production-Ready</CardTitle>\n              <CardDescription>\n                Not just a demo, but a real foundation for your applications\n              </CardDescription>\n            </CardHeader>\n            <CardContent>\n              <p className=\"text-sm text-muted-foreground\">\n                Every component, pattern, and configuration has been\n                battle-tested in production environments. Security, performance,\n                and maintainability are built-in from day one.\n              </p>\n            </CardContent>\n          </Card>\n\n          <Card>\n            <CardHeader>\n              <CardTitle>⚡ Edge-First Architecture</CardTitle>\n              <CardDescription>\n                Optimized for global performance at CDN edge locations\n              </CardDescription>\n            </CardHeader>\n            <CardContent>\n              <p className=\"text-sm text-muted-foreground\">\n                Built specifically for Cloudflare Workers and edge computing.\n                Your applications run closer to your users for lightning-fast\n                response times.\n              </p>\n            </CardContent>\n          </Card>\n\n          <Card>\n            <CardHeader>\n              <CardTitle>🔧 Developer Experience</CardTitle>\n              <CardDescription>\n                Carefully crafted tooling for maximum productivity\n              </CardDescription>\n            </CardHeader>\n            <CardContent>\n              <p className=\"text-sm text-muted-foreground\">\n                Hot reload, TypeScript support, comprehensive testing setup, and\n                intuitive project structure. Everything you need to stay in the\n                flow.\n              </p>\n            </CardContent>\n          </Card>\n\n          <Card>\n            <CardHeader>\n              <CardTitle>🌐 Full-Stack Solution</CardTitle>\n              <CardDescription>\n                Complete backend and frontend in one cohesive package\n              </CardDescription>\n            </CardHeader>\n            <CardContent>\n              <p className=\"text-sm text-muted-foreground\">\n                tRPC for type-safe APIs, Better Auth for authentication and\n                database, and WebSocket support for real-time features.\n              </p>\n            </CardContent>\n          </Card>\n        </div>\n      </section>\n\n      {/* Technology Choices */}\n      <section className=\"mb-20\">\n        <h2 className=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n          Technology Choices\n        </h2>\n\n        <Card>\n          <CardContent className=\"pt-6\">\n            <div className=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n              <div>\n                <h3 className=\"font-semibold mb-4\">Frontend Stack</h3>\n                <ul className=\"space-y-2 text-sm text-muted-foreground\">\n                  <li>\n                    <strong>React 19:</strong> Latest React with concurrent\n                    features\n                  </li>\n                  <li>\n                    <strong>TypeScript:</strong> Type safety and better\n                    developer experience\n                  </li>\n                  <li>\n                    <strong>Vite:</strong> Lightning-fast build tool and dev\n                    server\n                  </li>\n                  <li>\n                    <strong>TanStack Router:</strong> Type-safe routing with\n                    code splitting\n                  </li>\n                  <li>\n                    <strong>shadcn/ui:</strong> Beautiful, accessible component\n                    library\n                  </li>\n                  <li>\n                    <strong>Tailwind CSS:</strong> Utility-first CSS framework\n                  </li>\n                </ul>\n              </div>\n\n              <div>\n                <h3 className=\"font-semibold mb-4\">Backend Stack</h3>\n                <ul className=\"space-y-2 text-sm text-muted-foreground\">\n                  <li>\n                    <strong>Bun:</strong> Fast JavaScript runtime and package\n                    manager\n                  </li>\n                  <li>\n                    <strong>Hono:</strong> Ultra-fast web framework for edge\n                    computing\n                  </li>\n                  <li>\n                    <strong>tRPC:</strong> End-to-end type safety for APIs\n                  </li>\n                  <li>\n                    <strong>Better Auth:</strong> Authentication\n                  </li>\n                  <li>\n                    <strong>Cloudflare Workers:</strong> Serverless edge\n                    computing\n                  </li>\n                  <li>\n                    <strong>WebSockets:</strong> Real-time communication support\n                  </li>\n                </ul>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </section>\n\n      {/* Team Section */}\n      <section className=\"mb-20\">\n        <h2 className=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n          Built by Kriasoft\n        </h2>\n\n        <Card>\n          <CardContent className=\"pt-6 text-center\">\n            <p className=\"text-muted-foreground mb-6\">\n              React Starter Kit is maintained by Kriasoft, a team of experienced\n              developers passionate about modern web technologies and developer\n              experience.\n            </p>\n\n            <div className=\"flex flex-col sm:flex-row gap-4 justify-center\">\n              <Button asChild>\n                <a\n                  href=\"https://github.com/kriasoft\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  Visit Kriasoft on GitHub\n                </a>\n              </Button>\n              <Button variant=\"outline\" asChild>\n                <a\n                  href=\"https://kriasoft.com\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                >\n                  Learn More About Kriasoft\n                </a>\n              </Button>\n            </div>\n          </CardContent>\n        </Card>\n      </section>\n\n      <Separator className=\"my-12\" />\n\n      {/* CTA Section */}\n      <section className=\"text-center\">\n        <h2 className=\"text-3xl font-bold tracking-tight mb-4\">\n          Ready to Get Started?\n        </h2>\n        <p className=\"text-muted-foreground text-lg mb-8 max-w-2xl mx-auto\">\n          Join thousands of developers who have chosen React Starter Kit for\n          their next project.\n        </p>\n\n        <div className=\"flex flex-col sm:flex-row gap-4 justify-center\">\n          <Button size=\"lg\" asChild>\n            <a\n              href=\"https://github.com/kriasoft/react-starter-kit\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Get Started Now\n            </a>\n          </Button>\n          <Button variant=\"outline\" size=\"lg\" asChild>\n            <a\n              href=\"https://github.com/kriasoft/react-starter-kit/discussions\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Join the Community\n            </a>\n          </Button>\n        </div>\n      </section>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/analytics.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { Activity, DollarSign, TrendingUp, Users } from \"lucide-react\";\n\nexport const Route = createFileRoute(\"/(app)/analytics\")({\n  component: Analytics,\n});\n\nfunction Analytics() {\n  const metrics = [\n    {\n      title: \"Total Revenue\",\n      value: \"$45,231.89\",\n      change: \"+20.1% from last month\",\n      icon: DollarSign,\n      color: \"text-green-600\",\n    },\n    {\n      title: \"Active Users\",\n      value: \"2,350\",\n      change: \"+180 from last month\",\n      icon: Users,\n      color: \"text-blue-600\",\n    },\n    {\n      title: \"Conversion Rate\",\n      value: \"3.2%\",\n      change: \"+0.5% from last month\",\n      icon: TrendingUp,\n      color: \"text-purple-600\",\n    },\n    {\n      title: \"Avg. Session Duration\",\n      value: \"4m 32s\",\n      change: \"+12s from last month\",\n      icon: Activity,\n      color: \"text-orange-600\",\n    },\n  ];\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h2 className=\"text-2xl font-bold\">Analytics</h2>\n        <p className=\"text-muted-foreground\">\n          Track your application's performance and user engagement metrics.\n        </p>\n      </div>\n\n      {/* Metrics Grid */}\n      <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n        {metrics.map((metric) => (\n          <Card key={metric.title}>\n            <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n              <CardTitle className=\"text-sm font-medium\">\n                {metric.title}\n              </CardTitle>\n              <metric.icon className={`h-4 w-4 ${metric.color}`} />\n            </CardHeader>\n            <CardContent>\n              <div className=\"text-2xl font-bold\">{metric.value}</div>\n              <p className=\"text-xs text-muted-foreground\">{metric.change}</p>\n            </CardContent>\n          </Card>\n        ))}\n      </div>\n\n      {/* Charts Section */}\n      <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n        <Card>\n          <CardHeader>\n            <CardTitle>Revenue Overview</CardTitle>\n            <CardDescription>\n              Monthly revenue for the past 6 months\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <div className=\"h-64 flex items-center justify-center text-muted-foreground\">\n              {/* Placeholder for chart */}\n              <div className=\"text-center\">\n                <TrendingUp className=\"h-12 w-12 mx-auto mb-2\" />\n                <p>Chart visualization would go here</p>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle>User Growth</CardTitle>\n            <CardDescription>New vs returning users over time</CardDescription>\n          </CardHeader>\n          <CardContent>\n            <div className=\"h-64 flex items-center justify-center text-muted-foreground\">\n              {/* Placeholder for chart */}\n              <div className=\"text-center\">\n                <Users className=\"h-12 w-12 mx-auto mb-2\" />\n                <p>Chart visualization would go here</p>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n\n      {/* Top Pages */}\n      <Card>\n        <CardHeader>\n          <CardTitle>Top Pages</CardTitle>\n          <CardDescription>\n            Most visited pages in your application\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"space-y-4\">\n            {[\n              { page: \"/dashboard\", views: 12543, percentage: 35 },\n              { page: \"/products\", views: 8932, percentage: 25 },\n              { page: \"/settings\", views: 6421, percentage: 18 },\n              { page: \"/reports\", views: 4532, percentage: 13 },\n              { page: \"/about\", views: 3221, percentage: 9 },\n            ].map((item) => (\n              <div key={item.page} className=\"flex items-center gap-4\">\n                <div className=\"flex-1\">\n                  <div className=\"flex justify-between mb-1\">\n                    <span className=\"text-sm font-medium\">{item.page}</span>\n                    <span className=\"text-sm text-muted-foreground\">\n                      {item.views.toLocaleString()} views\n                    </span>\n                  </div>\n                  <div className=\"w-full bg-secondary rounded-full h-2\">\n                    <div\n                      className=\"bg-primary h-2 rounded-full\"\n                      style={{ width: `${item.percentage}%` }}\n                    />\n                  </div>\n                </div>\n              </div>\n            ))}\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/dashboard.tsx",
    "content": "import { createFileRoute, redirect } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(app)/dashboard\")({\n  beforeLoad: () => {\n    // Redirect to index which is the main dashboard\n    throw redirect({\n      to: \"/\",\n    });\n  },\n});\n"
  },
  {
    "path": "apps/app/routes/(app)/index.tsx",
    "content": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { Activity, FileText, TrendingUp, Users } from \"lucide-react\";\n\nexport const Route = createFileRoute(\"/(app)/\")({\n  component: Dashboard,\n});\n\nfunction Dashboard() {\n  const stats = [\n    {\n      title: \"Total Users\",\n      value: \"1,234\",\n      change: \"+12%\",\n      icon: Users,\n    },\n    {\n      title: \"Active Sessions\",\n      value: \"89\",\n      change: \"+5%\",\n      icon: Activity,\n    },\n    {\n      title: \"Reports Generated\",\n      value: \"456\",\n      change: \"+23%\",\n      icon: FileText,\n    },\n    {\n      title: \"Growth Rate\",\n      value: \"18.2%\",\n      change: \"+2.1%\",\n      icon: TrendingUp,\n    },\n  ];\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h2 className=\"text-2xl font-bold\">Dashboard</h2>\n        <p className=\"text-muted-foreground\">\n          Welcome back! Here's an overview of your application.\n        </p>\n      </div>\n\n      {/* Stats Grid */}\n      <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">\n        {stats.map((stat) => (\n          <Card key={stat.title}>\n            <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n              <CardTitle className=\"text-sm font-medium\">\n                {stat.title}\n              </CardTitle>\n              <stat.icon className=\"h-4 w-4 text-muted-foreground\" />\n            </CardHeader>\n            <CardContent>\n              <div className=\"text-2xl font-bold\">{stat.value}</div>\n              <p className=\"text-xs text-muted-foreground\">\n                <span className=\"text-green-600\">{stat.change}</span> from last\n                month\n              </p>\n            </CardContent>\n          </Card>\n        ))}\n      </div>\n\n      {/* Main Content Area */}\n      <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n        <Card>\n          <CardHeader>\n            <CardTitle>Recent Activity</CardTitle>\n            <CardDescription>Latest events in your application</CardDescription>\n          </CardHeader>\n          <CardContent>\n            <div className=\"space-y-4\">\n              {[1, 2, 3, 4].map((i) => (\n                <div key={i} className=\"flex items-center gap-4\">\n                  <div className=\"h-2 w-2 rounded-full bg-primary\" />\n                  <div className=\"flex-1\">\n                    <p className=\"text-sm\">User action performed</p>\n                    <p className=\"text-xs text-muted-foreground\">\n                      {i} hour{i > 1 ? \"s\" : \"\"} ago\n                    </p>\n                  </div>\n                </div>\n              ))}\n            </div>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle>Quick Actions</CardTitle>\n            <CardDescription>Common tasks and operations</CardDescription>\n          </CardHeader>\n          <CardContent>\n            <div className=\"grid grid-cols-2 gap-4\">\n              <button\n                type=\"button\"\n                className=\"p-4 text-left border rounded-lg hover:bg-accent transition-colors\"\n              >\n                <FileText className=\"h-5 w-5 mb-2\" />\n                <p className=\"text-sm font-medium\">Generate Report</p>\n              </button>\n              <button\n                type=\"button\"\n                className=\"p-4 text-left border rounded-lg hover:bg-accent transition-colors\"\n              >\n                <Users className=\"h-5 w-5 mb-2\" />\n                <p className=\"text-sm font-medium\">Manage Users</p>\n              </button>\n              <button\n                type=\"button\"\n                className=\"p-4 text-left border rounded-lg hover:bg-accent transition-colors\"\n              >\n                <Activity className=\"h-5 w-5 mb-2\" />\n                <p className=\"text-sm font-medium\">View Analytics</p>\n              </button>\n              <button\n                type=\"button\"\n                className=\"p-4 text-left border rounded-lg hover:bg-accent transition-colors\"\n              >\n                <TrendingUp className=\"h-5 w-5 mb-2\" />\n                <p className=\"text-sm font-medium\">Export Data</p>\n              </button>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/reports.tsx",
    "content": "import {\n  Button,\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { Calendar, Download, FileText, Filter } from \"lucide-react\";\n\nexport const Route = createFileRoute(\"/(app)/reports\")({\n  component: Reports,\n});\n\nfunction Reports() {\n  const reports = [\n    {\n      id: 1,\n      name: \"Monthly Sales Report\",\n      type: \"Sales\",\n      date: \"2024-01-01\",\n      status: \"Ready\",\n    },\n    {\n      id: 2,\n      name: \"User Activity Report\",\n      type: \"Analytics\",\n      date: \"2024-01-15\",\n      status: \"Ready\",\n    },\n    {\n      id: 3,\n      name: \"Financial Summary\",\n      type: \"Finance\",\n      date: \"2024-01-20\",\n      status: \"Processing\",\n    },\n    {\n      id: 4,\n      name: \"Performance Metrics\",\n      type: \"Performance\",\n      date: \"2024-01-25\",\n      status: \"Ready\",\n    },\n  ];\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h2 className=\"text-2xl font-bold\">Reports</h2>\n        <p className=\"text-muted-foreground\">\n          Generate and download various reports for your data.\n        </p>\n      </div>\n\n      {/* Filters */}\n      <Card>\n        <CardHeader>\n          <CardTitle>Filters</CardTitle>\n          <CardDescription>\n            Filter reports by type and date range\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"flex flex-col sm:flex-row gap-4\">\n            <div className=\"flex-1\">\n              <Select>\n                <SelectTrigger>\n                  <SelectValue placeholder=\"Select report type\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All Types</SelectItem>\n                  <SelectItem value=\"sales\">Sales</SelectItem>\n                  <SelectItem value=\"analytics\">Analytics</SelectItem>\n                  <SelectItem value=\"finance\">Finance</SelectItem>\n                  <SelectItem value=\"performance\">Performance</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n            <div className=\"flex-1\">\n              <Select>\n                <SelectTrigger>\n                  <SelectValue placeholder=\"Select date range\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"7days\">Last 7 days</SelectItem>\n                  <SelectItem value=\"30days\">Last 30 days</SelectItem>\n                  <SelectItem value=\"90days\">Last 90 days</SelectItem>\n                  <SelectItem value=\"year\">This year</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n            <Button className=\"gap-2\">\n              <Filter className=\"h-4 w-4\" />\n              Apply Filters\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n\n      {/* Report Generation */}\n      <Card>\n        <CardHeader>\n          <CardTitle>Generate New Report</CardTitle>\n          <CardDescription>\n            Create a custom report based on your needs\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n            <Button variant=\"outline\" className=\"h-24 flex-col gap-2\">\n              <FileText className=\"h-6 w-6\" />\n              <span>Sales Report</span>\n            </Button>\n            <Button variant=\"outline\" className=\"h-24 flex-col gap-2\">\n              <FileText className=\"h-6 w-6\" />\n              <span>User Report</span>\n            </Button>\n            <Button variant=\"outline\" className=\"h-24 flex-col gap-2\">\n              <FileText className=\"h-6 w-6\" />\n              <span>Financial Report</span>\n            </Button>\n            <Button variant=\"outline\" className=\"h-24 flex-col gap-2\">\n              <FileText className=\"h-6 w-6\" />\n              <span>Custom Report</span>\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n\n      {/* Recent Reports */}\n      <Card>\n        <CardHeader>\n          <CardTitle>Recent Reports</CardTitle>\n          <CardDescription>Your recently generated reports</CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"space-y-4\">\n            {reports.map((report) => (\n              <div\n                key={report.id}\n                className=\"flex items-center justify-between p-4 border rounded-lg\"\n              >\n                <div className=\"flex items-center gap-4\">\n                  <FileText className=\"h-8 w-8 text-muted-foreground\" />\n                  <div>\n                    <p className=\"font-medium\">{report.name}</p>\n                    <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n                      <span>{report.type}</span>\n                      <span className=\"flex items-center gap-1\">\n                        <Calendar className=\"h-3 w-3\" />\n                        {report.date}\n                      </span>\n                    </div>\n                  </div>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <span\n                    className={`px-2 py-1 text-xs rounded-full ${\n                      report.status === \"Ready\"\n                        ? \"bg-green-100 text-green-700\"\n                        : \"bg-yellow-100 text-yellow-700\"\n                    }`}\n                  >\n                    {report.status}\n                  </span>\n                  <Button\n                    size=\"sm\"\n                    variant=\"ghost\"\n                    disabled={report.status !== \"Ready\"}\n                  >\n                    <Download className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n              </div>\n            ))}\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/route.tsx",
    "content": "import { AuthErrorBoundary } from \"@/components/auth\";\nimport { Layout } from \"@/components/layout\";\nimport { getCachedSession, sessionQueryOptions } from \"@/lib/queries/session\";\nimport { createFileRoute, Outlet, redirect } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(app)\")({\n  // Route-level authentication guard using cache-first strategy.\n  // Checks cache before fetching to make navigation instant.\n  beforeLoad: async ({ context, location }) => {\n    let session = getCachedSession(context.queryClient);\n\n    if (session === undefined) {\n      session = await context.queryClient.fetchQuery(sessionQueryOptions());\n    }\n\n    // Both user and session must exist for valid auth state\n    if (!session?.user || !session?.session) {\n      throw redirect({\n        to: \"/login\",\n        search: { returnTo: location.href },\n      });\n    }\n\n    return { user: session.user, session };\n  },\n  component: AppLayout,\n});\n\nfunction AppLayout() {\n  return (\n    <AuthErrorBoundary>\n      <Layout>\n        <Outlet />\n      </Layout>\n    </AuthErrorBoundary>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/settings.tsx",
    "content": "import {\n  Button,\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n  Input,\n  Label,\n  Separator,\n  Switch,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { Bell, CreditCard, Palette, Shield, User } from \"lucide-react\";\nimport { auth } from \"@/lib/auth\";\nimport { useBillingQuery } from \"@/lib/queries/billing\";\nimport { useSessionQuery } from \"@/lib/queries/session\";\n\nexport const Route = createFileRoute(\"/(app)/settings\")({\n  component: Settings,\n});\n\nfunction Settings() {\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div>\n        <h2 className=\"text-2xl font-bold\">Settings</h2>\n        <p className=\"text-muted-foreground\">\n          Manage your account settings and preferences.\n        </p>\n      </div>\n\n      <div className=\"grid gap-6\">\n        {/* Profile Settings */}\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2\">\n              <User className=\"h-5 w-5\" />\n              <CardTitle>Profile</CardTitle>\n            </div>\n            <CardDescription>\n              Update your personal information and profile settings.\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div className=\"grid gap-2\">\n              <Label htmlFor=\"name\">Name</Label>\n              <Input id=\"name\" placeholder=\"Enter your name\" />\n            </div>\n            <div className=\"grid gap-2\">\n              <Label htmlFor=\"email\">Email</Label>\n              <Input id=\"email\" type=\"email\" placeholder=\"Enter your email\" />\n            </div>\n            <Button>Save Changes</Button>\n          </CardContent>\n        </Card>\n\n        {/* Billing */}\n        <BillingCard />\n\n        {/* Notification Settings */}\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2\">\n              <Bell className=\"h-5 w-5\" />\n              <CardTitle>Notifications</CardTitle>\n            </div>\n            <CardDescription>\n              Configure how you receive notifications.\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"space-y-0.5\">\n                <Label htmlFor=\"email-notifications\">Email Notifications</Label>\n                <p className=\"text-sm text-muted-foreground\">\n                  Receive notifications via email\n                </p>\n              </div>\n              <Switch id=\"email-notifications\" />\n            </div>\n            <Separator />\n            <div className=\"flex items-center justify-between\">\n              <div className=\"space-y-0.5\">\n                <Label htmlFor=\"push-notifications\">Push Notifications</Label>\n                <p className=\"text-sm text-muted-foreground\">\n                  Receive push notifications in your browser\n                </p>\n              </div>\n              <Switch id=\"push-notifications\" />\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* Security Settings */}\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2\">\n              <Shield className=\"h-5 w-5\" />\n              <CardTitle>Security</CardTitle>\n            </div>\n            <CardDescription>\n              Manage your security preferences and authentication.\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <Button variant=\"outline\">Change Password</Button>\n            </div>\n            <div className=\"space-y-2\">\n              <Button variant=\"outline\">\n                Enable Two-Factor Authentication\n              </Button>\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* Appearance Settings */}\n        <Card>\n          <CardHeader>\n            <div className=\"flex items-center gap-2\">\n              <Palette className=\"h-5 w-5\" />\n              <CardTitle>Appearance</CardTitle>\n            </div>\n            <CardDescription>\n              Customize the look and feel of the application.\n            </CardDescription>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"space-y-0.5\">\n                <Label htmlFor=\"dark-mode\">Dark Mode</Label>\n                <p className=\"text-sm text-muted-foreground\">\n                  Toggle dark mode theme\n                </p>\n              </div>\n              <Switch id=\"dark-mode\" />\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    </div>\n  );\n}\n\nfunction BillingCard() {\n  const { data: session } = useSessionQuery();\n  const activeOrgId = session?.session?.activeOrganizationId;\n  const { data: billing, isLoading } = useBillingQuery(activeOrgId);\n\n  const returnUrl = window.location.href;\n\n  async function handleUpgrade(plan: \"starter\" | \"pro\") {\n    try {\n      await auth.subscription.upgrade({\n        plan,\n        successUrl: returnUrl,\n        cancelUrl: returnUrl,\n      });\n    } catch (error) {\n      console.error(\"Failed to start upgrade:\", error);\n    }\n  }\n\n  async function handleManageBilling() {\n    try {\n      await auth.subscription.billingPortal({ returnUrl });\n    } catch (error) {\n      console.error(\"Failed to open billing portal:\", error);\n    }\n  }\n\n  const hasSubscription =\n    billing?.status === \"active\" || billing?.status === \"trialing\";\n  const isCanceling = hasSubscription && billing.cancelAtPeriodEnd;\n\n  return (\n    <Card>\n      <CardHeader>\n        <div className=\"flex items-center gap-2\">\n          <CreditCard className=\"h-5 w-5\" />\n          <CardTitle>Billing</CardTitle>\n        </div>\n        <CardDescription>\n          Manage your subscription and billing details.\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-4\">\n        {isLoading ? (\n          <p className=\"text-sm text-muted-foreground\">Loading...</p>\n        ) : hasSubscription ? (\n          <>\n            <div className=\"space-y-1\">\n              <p className=\"text-sm font-medium\">\n                {billing.plan.charAt(0).toUpperCase() + billing.plan.slice(1)}{\" \"}\n                plan\n                <span className=\"ml-2 text-xs text-muted-foreground\">\n                  ({billing.status})\n                </span>\n              </p>\n              {billing.periodEnd && (\n                <p className=\"text-sm text-muted-foreground\">\n                  {isCanceling ? \"Access until\" : \"Renews on\"}{\" \"}\n                  {new Date(billing.periodEnd).toLocaleDateString()}\n                </p>\n              )}\n              {isCanceling && (\n                <p className=\"text-sm text-amber-600\">\n                  Your subscription will not renew. You can restore it from the\n                  billing portal.\n                </p>\n              )}\n            </div>\n            <Button variant=\"outline\" onClick={handleManageBilling}>\n              Manage Billing\n            </Button>\n          </>\n        ) : (\n          <div className=\"space-y-3\">\n            <p className=\"text-sm text-muted-foreground\">\n              You are on the Free plan.\n            </p>\n            <div className=\"flex gap-2\">\n              <Button\n                variant=\"outline\"\n                onClick={() => handleUpgrade(\"starter\")}\n              >\n                Upgrade to Starter\n              </Button>\n              <Button onClick={() => handleUpgrade(\"pro\")}>\n                Upgrade to Pro\n              </Button>\n            </div>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(app)/users.tsx",
    "content": "import {\n  Avatar,\n  AvatarFallback,\n  Button,\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n  Input,\n} from \"@repo/ui\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport {\n  MoreVertical,\n  Search,\n  UserPlus,\n  Users as UsersIcon,\n} from \"lucide-react\";\n\nexport const Route = createFileRoute(\"/(app)/users\")({\n  component: Users,\n});\n\nfunction Users() {\n  const users = [\n    {\n      id: 1,\n      name: \"John Doe\",\n      email: \"john.doe@example.com\",\n      role: \"Admin\",\n      status: \"Active\",\n      lastActive: \"2 hours ago\",\n    },\n    {\n      id: 2,\n      name: \"Jane Smith\",\n      email: \"jane.smith@example.com\",\n      role: \"Editor\",\n      status: \"Active\",\n      lastActive: \"5 minutes ago\",\n    },\n    {\n      id: 3,\n      name: \"Bob Johnson\",\n      email: \"bob.johnson@example.com\",\n      role: \"Viewer\",\n      status: \"Inactive\",\n      lastActive: \"2 days ago\",\n    },\n    {\n      id: 4,\n      name: \"Alice Brown\",\n      email: \"alice.brown@example.com\",\n      role: \"Editor\",\n      status: \"Active\",\n      lastActive: \"1 hour ago\",\n    },\n    {\n      id: 5,\n      name: \"Charlie Wilson\",\n      email: \"charlie.wilson@example.com\",\n      role: \"Viewer\",\n      status: \"Active\",\n      lastActive: \"30 minutes ago\",\n    },\n  ];\n\n  return (\n    <div className=\"p-6 space-y-6\">\n      <div className=\"flex justify-between items-center\">\n        <div>\n          <h2 className=\"text-2xl font-bold\">Users</h2>\n          <p className=\"text-muted-foreground\">\n            Manage user accounts and permissions.\n          </p>\n        </div>\n        <Button className=\"gap-2\">\n          <UserPlus className=\"h-4 w-4\" />\n          Add User\n        </Button>\n      </div>\n\n      {/* Stats */}\n      <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n        <Card>\n          <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n            <CardTitle className=\"text-sm font-medium\">Total Users</CardTitle>\n            <UsersIcon className=\"h-4 w-4 text-muted-foreground\" />\n          </CardHeader>\n          <CardContent>\n            <div className=\"text-2xl font-bold\">1,234</div>\n            <p className=\"text-xs text-muted-foreground\">\n              +10% from last month\n            </p>\n          </CardContent>\n        </Card>\n        <Card>\n          <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n            <CardTitle className=\"text-sm font-medium\">Active Users</CardTitle>\n            <UsersIcon className=\"h-4 w-4 text-green-600\" />\n          </CardHeader>\n          <CardContent>\n            <div className=\"text-2xl font-bold\">892</div>\n            <p className=\"text-xs text-muted-foreground\">72% of total users</p>\n          </CardContent>\n        </Card>\n        <Card>\n          <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n            <CardTitle className=\"text-sm font-medium\">\n              New This Month\n            </CardTitle>\n            <UserPlus className=\"h-4 w-4 text-blue-600\" />\n          </CardHeader>\n          <CardContent>\n            <div className=\"text-2xl font-bold\">48</div>\n            <p className=\"text-xs text-muted-foreground\">\n              +32% from last month\n            </p>\n          </CardContent>\n        </Card>\n      </div>\n\n      {/* User List */}\n      <Card>\n        <CardHeader>\n          <CardTitle>User Management</CardTitle>\n          <CardDescription>View and manage all user accounts</CardDescription>\n        </CardHeader>\n        <CardContent>\n          {/* Search Bar */}\n          <div className=\"flex gap-4 mb-6\">\n            <div className=\"relative flex-1\">\n              <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n              <Input placeholder=\"Search users...\" className=\"pl-10\" />\n            </div>\n            <Button variant=\"outline\">Filter</Button>\n          </div>\n\n          {/* Users Table */}\n          <div className=\"border rounded-lg\">\n            <div className=\"overflow-x-auto\">\n              <table className=\"w-full\">\n                <thead>\n                  <tr className=\"border-b bg-muted/50\">\n                    <th className=\"text-left p-4 font-medium\">User</th>\n                    <th className=\"text-left p-4 font-medium\">Role</th>\n                    <th className=\"text-left p-4 font-medium\">Status</th>\n                    <th className=\"text-left p-4 font-medium\">Last Active</th>\n                    <th className=\"text-left p-4 font-medium\">Actions</th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {users.map((user) => (\n                    <tr key={user.id} className=\"border-b\">\n                      <td className=\"p-4\">\n                        <div className=\"flex items-center gap-3\">\n                          <Avatar>\n                            <AvatarFallback>\n                              {user.name\n                                .split(\" \")\n                                .map((n) => n[0])\n                                .join(\"\")}\n                            </AvatarFallback>\n                          </Avatar>\n                          <div>\n                            <p className=\"font-medium\">{user.name}</p>\n                            <p className=\"text-sm text-muted-foreground\">\n                              {user.email}\n                            </p>\n                          </div>\n                        </div>\n                      </td>\n                      <td className=\"p-4\">\n                        <span className=\"px-2 py-1 text-xs font-medium rounded-full bg-secondary\">\n                          {user.role}\n                        </span>\n                      </td>\n                      <td className=\"p-4\">\n                        <span\n                          className={`px-2 py-1 text-xs font-medium rounded-full ${\n                            user.status === \"Active\"\n                              ? \"bg-green-100 text-green-700\"\n                              : \"bg-gray-100 text-gray-700\"\n                          }`}\n                        >\n                          {user.status}\n                        </span>\n                      </td>\n                      <td className=\"p-4 text-sm text-muted-foreground\">\n                        {user.lastActive}\n                      </td>\n                      <td className=\"p-4\">\n                        <Button variant=\"ghost\" size=\"sm\">\n                          <MoreVertical className=\"h-4 w-4\" />\n                        </Button>\n                      </td>\n                    </tr>\n                  ))}\n                </tbody>\n              </table>\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(auth)/login.tsx",
    "content": "import { AuthForm } from \"@/components/auth\";\nimport { getSafeRedirectUrl } from \"@/lib/auth-config\";\nimport { revalidateSession, sessionQueryOptions } from \"@/lib/queries/session\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport {\n  createFileRoute,\n  isRedirect,\n  redirect,\n  useRouter,\n} from \"@tanstack/react-router\";\nimport { z } from \"zod\";\n\n// Sanitize returnTo at parse time - consumers get a safe value or undefined\nconst searchSchema = z.object({\n  returnTo: z\n    .string()\n    .optional()\n    .transform((val) => {\n      const safe = getSafeRedirectUrl(val);\n      return safe === \"/\" ? undefined : safe;\n    })\n    .catch(undefined),\n});\n\nexport const Route = createFileRoute(\"/(auth)/login\")({\n  validateSearch: searchSchema,\n  beforeLoad: async ({ context, search }) => {\n    try {\n      const session = await context.queryClient.fetchQuery(\n        sessionQueryOptions(),\n      );\n\n      // Redirect authenticated users to their destination\n      if (session?.user && session?.session) {\n        throw redirect({ to: search.returnTo ?? \"/\" });\n      }\n    } catch (error) {\n      // Re-throw redirects, show login form for fetch errors\n      if (isRedirect(error)) throw error;\n    }\n  },\n  component: LoginPage,\n});\n\nfunction LoginPage() {\n  const router = useRouter();\n  const queryClient = useQueryClient();\n  const search = Route.useSearch();\n\n  async function handleSuccess() {\n    await revalidateSession(queryClient, router);\n    await router.navigate({ to: search.returnTo ?? \"/\" });\n  }\n\n  return (\n    <div className=\"flex min-h-svh flex-col items-center justify-center bg-muted/40 p-6 md:p-10\">\n      <div className=\"w-full max-w-sm rounded-xl bg-background p-8 shadow-sm ring-1 ring-border/50\">\n        <AuthForm\n          mode=\"login\"\n          onSuccess={handleSuccess}\n          returnTo={search.returnTo}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/(auth)/signup.tsx",
    "content": "import { AuthForm } from \"@/components/auth\";\nimport { getSafeRedirectUrl } from \"@/lib/auth-config\";\nimport { revalidateSession, sessionQueryOptions } from \"@/lib/queries/session\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport {\n  createFileRoute,\n  isRedirect,\n  redirect,\n  useRouter,\n} from \"@tanstack/react-router\";\nimport { z } from \"zod\";\n\n// Sanitize returnTo at parse time - consumers get a safe value or undefined\nconst searchSchema = z.object({\n  returnTo: z\n    .string()\n    .optional()\n    .transform((val) => {\n      const safe = getSafeRedirectUrl(val);\n      return safe === \"/\" ? undefined : safe;\n    })\n    .catch(undefined),\n});\n\nexport const Route = createFileRoute(\"/(auth)/signup\")({\n  validateSearch: searchSchema,\n  beforeLoad: async ({ context, search }) => {\n    try {\n      const session = await context.queryClient.fetchQuery(\n        sessionQueryOptions(),\n      );\n\n      // Redirect authenticated users to their destination\n      if (session?.user && session?.session) {\n        throw redirect({ to: search.returnTo ?? \"/\" });\n      }\n    } catch (error) {\n      // Re-throw redirects, show signup form for fetch errors\n      if (isRedirect(error)) throw error;\n    }\n  },\n  component: SignupPage,\n});\n\nfunction SignupPage() {\n  const router = useRouter();\n  const queryClient = useQueryClient();\n  const search = Route.useSearch();\n\n  async function handleSuccess() {\n    await revalidateSession(queryClient, router);\n    await router.navigate({ to: search.returnTo ?? \"/\" });\n  }\n\n  return (\n    <div className=\"flex min-h-svh flex-col items-center justify-center bg-muted/40 p-6 md:p-10\">\n      <div className=\"w-full max-w-sm rounded-xl bg-background p-8 shadow-sm ring-1 ring-border/50\">\n        <AuthForm\n          mode=\"signup\"\n          onSuccess={handleSuccess}\n          returnTo={search.returnTo}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/app/routes/__root.tsx",
    "content": "import { AppErrorBoundary } from \"@/components/auth\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport { createRootRouteWithContext, Outlet } from \"@tanstack/react-router\";\nimport { TanStackRouterDevtools } from \"@tanstack/react-router-devtools\";\n\n// Only queryClient in context - needed for beforeLoad prefetching.\n// Auth client is a singleton (no hook equivalent in Better Auth).\nexport const Route = createRootRouteWithContext<{\n  queryClient: QueryClient;\n}>()({\n  component: Root,\n});\n\nexport function Root() {\n  return (\n    <AppErrorBoundary>\n      <Outlet />\n      {import.meta.env.DEV && <TanStackRouterDevtools />}\n    </AppErrorBoundary>\n  );\n}\n"
  },
  {
    "path": "apps/app/styles/globals.css",
    "content": "@import \"../tailwind.config.css\";\n\n/**\n * CSS Variables for ShadCN UI Theming\n *\n * These variables define the color scheme for light and dark modes.\n * They are referenced by the UI components and mapped to Tailwind\n * utilities in tailwind.config.css.\n *\n * Using oklch() for better color interpolation and consistency.\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch\n */\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "apps/app/tailwind.config.css",
    "content": "/**\n * Tailwind CSS v4 configuration for the main app.\n * @see https://tailwindcss.com/docs/v4-beta\n */\n\n@import \"tailwindcss\";\n\n/* Content paths for Tailwind to scan */\n@source \"./lib/**/*.{js,ts,jsx,tsx}\";\n@source \"./routes/**/*.{js,ts,jsx,tsx}\";\n@source \"./components/**/*.{js,ts,jsx,tsx}\";\n@source \"./index.html\";\n@source \"./index.tsx\";\n@source \"../../packages/ui/components/**/*.{ts,tsx}\";\n@source \"../../packages/ui/lib/**/*.{ts,tsx}\";\n@source \"../../packages/ui/hooks/**/*.{ts,tsx}\";\n\n/* Custom dark mode variant */\n@custom-variant dark (&:is(.dark *));\n\n/* Theme configuration */\n@theme inline {\n  /* Border radius values */\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n\n  /* Color mappings for Tailwind utilities */\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n"
  },
  {
    "path": "apps/app/tsconfig.json",
    "content": "{\n  \"extends\": \"../../packages/typescript-config/react.jsonc\",\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"tsBuildInfoFile\": \"../../.cache/tsconfig/web.tsbuildinfo\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"@repo/api\": [\"../api\"],\n      \"@repo/api/*\": [\"../api/*\"],\n      \"@repo/core\": [\"../../packages/core\"],\n      \"@repo/core/*\": [\"../../packages/core/*\"],\n      \"@repo/db\": [\"../../db\"],\n      \"@repo/db/*\": [\"../../db/*\"],\n      \"@repo/ui\": [\"../../packages/ui\"],\n      \"@repo/ui/*\": [\"../../packages/ui/*\"],\n      \"@repo/ws-protocol\": [\"../../packages/ws-protocol\"],\n      \"@repo/ws-protocol/*\": [\"../../packages/ws-protocol/*\"]\n    }\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.json\", \"./global.d.ts\"],\n  \"exclude\": [\"**/dist/**/*\", \"**/node_modules/**/*\"],\n  \"references\": [{ \"path\": \"../api\" }, { \"path\": \"../../packages/ui\" }]\n}\n"
  },
  {
    "path": "apps/app/vite.config.ts",
    "content": "import { tanstackRouter } from \"@tanstack/router-plugin/vite\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport { TLSSocket } from \"node:tls\";\nimport { URL, fileURLToPath } from \"node:url\";\nimport { loadEnv } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineProject } from \"vitest/config\";\n\nconst publicEnvVars = [\n  \"APP_NAME\",\n  \"APP_ORIGIN\",\n  \"GOOGLE_CLOUD_PROJECT\",\n  \"GA_MEASUREMENT_ID\",\n];\n\n/**\n * Vite configuration.\n * https://vitejs.dev/config/\n */\nexport default defineProject(({ mode }) => {\n  const envDir = fileURLToPath(new URL(\"../..\", import.meta.url));\n  const env = loadEnv(mode, envDir, \"\");\n\n  publicEnvVars.forEach((key) => {\n    if (!env[key]) throw new Error(`Missing environment variable: ${key}`);\n    process.env[`VITE_${key}`] = env[key];\n  });\n\n  return {\n    cacheDir: fileURLToPath(new URL(\"../../.cache/vite-app\", import.meta.url)),\n\n    build: {\n      rollupOptions: {\n        output: {\n          assetFileNames: \"_app/assets/[name]-[hash][extname]\",\n          chunkFileNames: \"_app/assets/[name]-[hash].js\",\n          entryFileNames: \"_app/assets/[name]-[hash].js\",\n          manualChunks: {\n            react: [\"react\", \"react-dom\"],\n            tanstack: [\"@tanstack/react-router\"],\n            ui: [\n              \"@radix-ui/react-slot\",\n              \"class-variance-authority\",\n              \"clsx\",\n              \"tailwind-merge\",\n            ],\n          },\n        },\n      },\n    },\n\n    resolve: {\n      conditions: [\"module\", \"browser\", \"development|production\"],\n    },\n\n    css: {\n      postcss: \"./postcss.config.js\",\n    },\n\n    plugins: [\n      tsconfigPaths(),\n      tanstackRouter({\n        routesDirectory: \"./routes\",\n        generatedRouteTree: \"./lib/routeTree.gen.ts\",\n        routeFileIgnorePrefix: \"-\",\n        quoteStyle: \"single\",\n        semicolons: false,\n        autoCodeSplitting: true,\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      }) as any,\n      // https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc\n      react(),\n    ],\n\n    server: {\n      proxy: {\n        // Proxy API requests to the backend server during development\n        \"/api\": {\n          target: env.API_ORIGIN,\n          changeOrigin: true,\n          configure(proxy) {\n            proxy.on(\"proxyReq\", (proxyReq, req) => {\n              // Forward the frontend's origin to the API server\n              // This allows the API to know the actual client origin for:\n              // - CORS configuration\n              // - Better Auth baseURL and trustedOrigins\n              // - Redirect URLs and callbacks\n              const proto = req.socket instanceof TLSSocket ? \"https\" : \"http\";\n              const host = req.headers.host || \"\";\n              const origin = req.headers.origin || `${proto}://${host}`;\n              proxyReq.setHeader(\"x-forwarded-origin\", origin);\n            });\n          },\n        },\n      },\n    },\n\n    test: {\n      environment: \"happy-dom\",\n      setupFiles: [\"./vitest.setup.ts\"],\n    },\n  };\n});\n"
  },
  {
    "path": "apps/app/vitest.setup.ts",
    "content": "import \"@testing-library/jest-dom/vitest\";\n"
  },
  {
    "path": "apps/app/wrangler.jsonc",
    "content": "{\n  \"$schema\": \"../../node_modules/wrangler/config-schema.json\",\n\n  // [METADATA]\n  // App worker accessed via service binding from web worker (no routes needed).\n  \"name\": \"example-app\",\n  \"compatibility_date\": \"2025-08-15\",\n  \"compatibility_flags\": [],\n  \"workers_dev\": false,\n\n  // [ASSETS]\n  // Serves bundled JavaScript, CSS, images, and other static assets.\n  \"assets\": {\n    \"directory\": \"./dist\",\n    \"not_found_handling\": \"single-page-application\"\n  },\n\n  // [ENV:PRODUCTION]\n  // Command: bun wrangler deploy\n  \"vars\": {\n    \"ENVIRONMENT\": \"production\",\n    \"ALLOWED_ORIGINS\": \"https://example.com\"\n  },\n\n  \"env\": {\n    // [ENV:DEVELOPMENT]\n    // Command: bun wrangler dev\n    \"dev\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"development\",\n        \"ALLOWED_ORIGINS\": \"http://localhost:5173,http://127.0.0.1:5173\"\n      }\n    },\n\n    // [ENV:STAGING]\n    // Command: bun wrangler deploy --env staging\n    \"staging\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"staging\",\n        \"ALLOWED_ORIGINS\": \"https://staging.example.com\"\n      }\n    },\n\n    // [ENV:PREVIEW]\n    // Command: bun wrangler deploy --env preview\n    \"preview\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"preview\",\n        \"ALLOWED_ORIGINS\": \"https://preview.example.com\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/email/README.md",
    "content": "# Email Templates\n\nTransactional email templates built with React Email.\n\n[Documentation](https://reactstarter.com/email)\n\n## Templates\n\n- **EmailVerification** - Email verification with verification link\n- **PasswordReset** - Password reset with secure reset link\n- **OTPEmail** - One-time password codes for sign-in, verification, or password reset\n\n## Development\n\n```bash\n# Start email preview development server\nbun email:dev\n\n# Build email templates\nbun email:build\n\n# Export static email templates\nbun email:export\n```\n\nThe development server will be available at <http://localhost:3001>\n\n## Usage\n\n```typescript\nimport { EmailVerification, renderEmailToHtml } from \"@repo/email\";\n\nconst component = EmailVerification({\n  userName: \"John Doe\",\n  verificationUrl: \"https://example.com/verify?token=abc123\",\n  appName: \"My App\",\n  appUrl: \"https://example.com\",\n});\n\nconst html = await renderEmailToHtml(component);\n```\n\n## Structure\n\n```bash\ntemplates/        # React Email component templates\ncomponents/       # Shared components (BaseTemplate)\nutils/            # Rendering utilities\nemails/           # Preview files for development server\n```\n"
  },
  {
    "path": "apps/email/components/BaseTemplate.tsx",
    "content": "import {\n  Body,\n  Container,\n  Head,\n  Hr,\n  Html,\n  Img,\n  Link,\n  Preview,\n  Section,\n  Text,\n} from \"@react-email/components\";\nimport type { ReactNode } from \"react\";\n\ninterface BaseTemplateProps {\n  preview: string;\n  children: ReactNode;\n  appName?: string;\n  appUrl?: string;\n}\n\n// Color constants for consistent styling\nconst colors = {\n  primary: \"#007bff\",\n  danger: \"#dc3545\",\n  text: \"#32325d\",\n  textMuted: \"#525f7f\",\n  textLight: \"#8898aa\",\n  border: \"#e6e8eb\",\n  background: \"#f6f9fc\",\n  white: \"#ffffff\",\n  warning: \"#fff3cd\",\n  warningBorder: \"#ffeaa7\",\n} as const;\n\nexport function BaseTemplate({\n  preview,\n  children,\n  appName = \"React Starter Kit\",\n  appUrl = \"https://example.com\",\n}: BaseTemplateProps) {\n  // Embedded SVG logo as data URI for better email compatibility\n  const logoDataUri =\n    \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8Y2lyY2xlIGN4PSIyMCIgY3k9IjIwIiByPSIyMCIgZmlsbD0iIzAwN2JmZiIvPgogIDx0ZXh0IHg9IjIwIiB5PSIyNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0id2hpdGUiIGZvbnQtZmFtaWx5PSItYXBwbGUtc3lzdGVtLEJsaW5rTWFjU3lzdGVtRm9udCwnU2Vnb2UgVUknLFJvYm90bywnSGVsdmV0aWNhIE5ldWUnLFVidW50dSxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4IiBmb250LXdlaWdodD0iNjAwIj5SPC90ZXh0Pgo8L3N2Zz4K\";\n\n  return (\n    <Html>\n      <Head />\n      <Preview>{preview}</Preview>\n      <Body style={main}>\n        <Container style={container}>\n          {/* Header */}\n          <Section style={header}>\n            <Img\n              src={logoDataUri}\n              width=\"40\"\n              height=\"40\"\n              alt={appName}\n              style={logo}\n            />\n            <Text style={headerText}>{appName}</Text>\n          </Section>\n\n          {/* Main Content */}\n          <Section style={content}>{children}</Section>\n\n          {/* Footer */}\n          <Hr style={hr} />\n          <Section style={footer}>\n            <Text style={footerText}>\n              This email was sent by {appName}. If you didn't expect this email,\n              you can safely ignore it.\n            </Text>\n            {/* NOTE: Links assume standard /unsubscribe and /privacy routes exist */}\n            {appUrl && (\n              <Text style={footerText}>\n                <Link href={`${appUrl}/unsubscribe`} style={footerLink}>\n                  Unsubscribe\n                </Link>{\" \"}\n                |{\" \"}\n                <Link href={`${appUrl}/privacy`} style={footerLink}>\n                  Privacy Policy\n                </Link>\n              </Text>\n            )}\n          </Section>\n        </Container>\n      </Body>\n    </Html>\n  );\n}\n\nconst main = {\n  backgroundColor: colors.background,\n  fontFamily:\n    '-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Ubuntu,sans-serif',\n};\n\nconst container = {\n  backgroundColor: colors.white,\n  margin: \"0 auto\",\n  padding: \"20px 0 48px\",\n  marginBottom: \"64px\",\n  maxWidth: \"600px\",\n};\n\nconst header = {\n  padding: \"0 48px\",\n  textAlign: \"center\" as const,\n  borderBottom: `1px solid ${colors.border}`,\n  paddingBottom: \"20px\",\n  marginBottom: \"32px\",\n};\n\nconst logo = {\n  margin: \"0 auto 8px auto\",\n  display: \"block\",\n};\n\nconst headerText = {\n  fontSize: \"24px\",\n  fontWeight: \"600\",\n  color: colors.text,\n  margin: \"0\",\n  textAlign: \"center\" as const,\n};\n\nconst content = {\n  padding: \"0 48px\",\n};\n\nconst hr = {\n  borderColor: colors.border,\n  margin: \"20px 0\",\n};\n\nconst footer = {\n  padding: \"0 48px\",\n};\n\nconst footerText = {\n  color: colors.textLight,\n  fontSize: \"12px\",\n  lineHeight: \"16px\",\n  textAlign: \"center\" as const,\n  margin: \"0 0 8px 0\",\n};\n\nconst footerLink = {\n  color: colors.primary,\n  textDecoration: \"underline\",\n};\n\nexport { colors };\n"
  },
  {
    "path": "apps/email/emails/email-verification.tsx",
    "content": "import { EmailVerification } from \"../templates/email-verification\";\n\nexport default function EmailVerificationPreview() {\n  return (\n    <EmailVerification\n      userName=\"John Doe\"\n      verificationUrl=\"https://example.com/verify?token=abc123\"\n      appName=\"React Starter Kit\"\n      appUrl=\"https://example.com\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/email/emails/otp-password-reset.tsx",
    "content": "import { OTPEmail } from \"../templates/otp-email\";\n\nexport default function OTPPasswordResetPreview() {\n  return (\n    <OTPEmail\n      otp=\"456789\"\n      type=\"forget-password\"\n      appName=\"React Starter Kit\"\n      appUrl=\"https://example.com\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/email/emails/otp-sign-in.tsx",
    "content": "import { OTPEmail } from \"../templates/otp-email\";\n\nexport default function OTPSignInPreview() {\n  return (\n    <OTPEmail\n      otp=\"123456\"\n      type=\"sign-in\"\n      appName=\"React Starter Kit\"\n      appUrl=\"https://example.com\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/email/emails/otp-verification.tsx",
    "content": "import { OTPEmail } from \"../templates/otp-email\";\n\nexport default function OTPVerificationPreview() {\n  return (\n    <OTPEmail\n      otp=\"789012\"\n      type=\"email-verification\"\n      appName=\"React Starter Kit\"\n      appUrl=\"https://example.com\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/email/emails/password-reset.tsx",
    "content": "import { PasswordReset } from \"../templates/password-reset\";\n\nexport default function PasswordResetPreview() {\n  return (\n    <PasswordReset\n      userName=\"John Doe\"\n      resetUrl=\"https://example.com/reset?token=xyz789\"\n      appName=\"React Starter Kit\"\n      appUrl=\"https://example.com\"\n    />\n  );\n}\n"
  },
  {
    "path": "apps/email/index.ts",
    "content": "export { EmailVerification } from \"./templates/email-verification.js\";\nexport { PasswordReset } from \"./templates/password-reset.js\";\nexport { OTPEmail } from \"./templates/otp-email.js\";\nexport { renderEmailToHtml, renderEmailToText } from \"./utils/render.js\";\n"
  },
  {
    "path": "apps/email/package.json",
    "content": "{\n  \"name\": \"@repo/email\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\"\n    },\n    \"./templates/*\": {\n      \"types\": \"./dist/templates/*.d.ts\",\n      \"import\": \"./dist/templates/*.js\"\n    },\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"dev\": \"bunx react-email dev --port 3001\",\n    \"build\": \"tsc\",\n    \"export\": \"bunx react-email export\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@react-email/components\": \"^1.0.8\",\n    \"@react-email/render\": \"^2.0.4\",\n    \"react\": \"19.2.4\"\n  },\n  \"devDependencies\": {\n    \"@react-email/preview-server\": \"^5.2.8\",\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/react\": \"^19.2.14\",\n    \"react-email\": \"^5.2.8\",\n    \"typescript\": \"~5.9.3\"\n  }\n}\n"
  },
  {
    "path": "apps/email/templates/email-verification.tsx",
    "content": "import { Button, Heading, Section, Text } from \"@react-email/components\";\nimport { BaseTemplate, colors } from \"../components/BaseTemplate\";\n\ninterface EmailVerificationProps {\n  userName?: string;\n  verificationUrl: string;\n  appName?: string;\n  appUrl?: string;\n}\n\nexport function EmailVerification({\n  userName,\n  verificationUrl,\n  appName,\n  appUrl,\n}: EmailVerificationProps) {\n  const preview = `Verify your email address for ${appName || \"your account\"}`;\n\n  return (\n    <BaseTemplate preview={preview} appName={appName} appUrl={appUrl}>\n      <Heading style={heading}>Verify your email address</Heading>\n\n      <Text style={paragraph}>Hi{userName ? ` ${userName}` : \"\"},</Text>\n\n      <Text style={paragraph}>\n        Thanks for signing up! Please click the button below to verify your\n        email address and complete your account setup.\n      </Text>\n\n      <Section style={buttonContainer}>\n        <Button href={verificationUrl} style={button}>\n          Verify Email Address\n        </Button>\n      </Section>\n\n      <Text style={paragraph}>\n        Or copy and paste this URL into your browser:\n      </Text>\n\n      <Text style={linkText}>{verificationUrl}</Text>\n\n      <Text style={paragraph}>\n        This verification link will expire in 24 hours for security reasons.\n      </Text>\n\n      <Text style={paragraph}>\n        If you didn't create an account with us, you can safely ignore this\n        email.\n      </Text>\n    </BaseTemplate>\n  );\n}\n\nconst heading = {\n  fontSize: \"24px\",\n  fontWeight: \"600\",\n  color: colors.text,\n  margin: \"0 0 24px\",\n};\n\nconst paragraph = {\n  fontSize: \"16px\",\n  lineHeight: \"24px\",\n  color: colors.textMuted,\n  margin: \"0 0 16px\",\n};\n\nconst buttonContainer = {\n  textAlign: \"center\" as const,\n  margin: \"32px 0\",\n};\n\nconst button = {\n  backgroundColor: colors.primary,\n  borderRadius: \"6px\",\n  color: colors.white,\n  fontSize: \"16px\",\n  fontWeight: \"600\",\n  textDecoration: \"none\",\n  textAlign: \"center\" as const,\n  display: \"inline-block\",\n  padding: \"12px 24px\",\n  lineHeight: \"20px\",\n};\n\nconst linkText = {\n  fontSize: \"14px\",\n  color: colors.textLight,\n  wordBreak: \"break-all\" as const,\n  margin: \"0 0 16px\",\n  padding: \"12px\",\n  backgroundColor: \"#f8f9fa\",\n  borderRadius: \"4px\",\n  border: \"1px solid #e9ecef\",\n};\n"
  },
  {
    "path": "apps/email/templates/otp-email.tsx",
    "content": "import { Heading, Section, Text } from \"@react-email/components\";\nimport { BaseTemplate, colors } from \"../components/BaseTemplate\";\n\ninterface OTPEmailProps {\n  otp: string;\n  type: \"sign-in\" | \"email-verification\" | \"forget-password\";\n  appName?: string;\n  appUrl?: string;\n  expiresInMinutes?: number;\n}\n\nexport function OTPEmail({\n  otp,\n  type,\n  appName,\n  appUrl,\n  expiresInMinutes = 5,\n}: OTPEmailProps) {\n  // [CONTENT_MAPPING] Maps type enum to user-facing labels and descriptions\n  const typeLabels = {\n    \"sign-in\": \"Sign In\",\n    \"email-verification\": \"Email Verification\",\n    \"forget-password\": \"Password Reset\",\n  };\n\n  const typeDescriptions = {\n    \"sign-in\": \"complete your sign in\",\n    \"email-verification\": \"verify your email address\",\n    \"forget-password\": \"reset your password\",\n  };\n\n  const typeLabel = typeLabels[type];\n  const typeDescription = typeDescriptions[type];\n  const preview = `Your ${typeLabel} code: ${otp}`;\n\n  return (\n    <BaseTemplate preview={preview} appName={appName} appUrl={appUrl}>\n      <Heading style={heading}>Your {typeLabel} Code</Heading>\n\n      <Text style={paragraph}>\n        Use the verification code below to {typeDescription}:\n      </Text>\n\n      <Section style={otpContainer}>\n        <Text style={otpText}>{otp}</Text>\n      </Section>\n\n      <Text style={paragraph}>\n        <strong>This code will expire in {expiresInMinutes} minutes</strong> for\n        security reasons.\n      </Text>\n\n      <Text style={paragraph}>\n        If you didn't request this code, you can safely ignore this email.\n      </Text>\n\n      {/* WARNING: Security notice shown only for password reset to emphasize risk */}\n      {type === \"forget-password\" && (\n        <Text style={securityNote}>\n          <strong>Security tip:</strong> Never share this verification code with\n          anyone. Our support team will never ask for your verification codes.\n        </Text>\n      )}\n    </BaseTemplate>\n  );\n}\n\nconst heading = {\n  fontSize: \"24px\",\n  fontWeight: \"600\",\n  color: colors.text,\n  margin: \"0 0 24px\",\n};\n\nconst paragraph = {\n  fontSize: \"16px\",\n  lineHeight: \"24px\",\n  color: colors.textMuted,\n  margin: \"0 0 16px\",\n};\n\nconst otpContainer = {\n  textAlign: \"center\" as const,\n  margin: \"32px 0\",\n  padding: \"24px\",\n  backgroundColor: \"#f8f9fa\",\n  border: \"2px solid #e9ecef\",\n  borderRadius: \"8px\",\n};\n\nconst otpText = {\n  fontSize: \"36px\",\n  fontWeight: \"bold\",\n  letterSpacing: \"0.5em\",\n  color: colors.primary,\n  fontFamily: \"Monaco, Consolas, monospace\",\n  margin: \"0\",\n  textAlign: \"center\" as const,\n};\n\nconst securityNote = {\n  fontSize: \"14px\",\n  lineHeight: \"20px\",\n  color: \"#6c757d\",\n  margin: \"24px 0 0\",\n  padding: \"16px\",\n  backgroundColor: colors.warning,\n  borderRadius: \"4px\",\n  border: `1px solid ${colors.warningBorder}`,\n};\n"
  },
  {
    "path": "apps/email/templates/password-reset.tsx",
    "content": "import { Button, Heading, Section, Text } from \"@react-email/components\";\nimport { BaseTemplate, colors } from \"../components/BaseTemplate\";\n\ninterface PasswordResetProps {\n  userName?: string;\n  resetUrl: string;\n  appName?: string;\n  appUrl?: string;\n}\n\nexport function PasswordReset({\n  userName,\n  resetUrl,\n  appName,\n  appUrl,\n}: PasswordResetProps) {\n  const preview = `Reset your password for ${appName || \"your account\"}`;\n\n  return (\n    <BaseTemplate preview={preview} appName={appName} appUrl={appUrl}>\n      <Heading style={heading}>Reset your password</Heading>\n\n      <Text style={paragraph}>Hi{userName ? ` ${userName}` : \"\"},</Text>\n\n      <Text style={paragraph}>\n        We received a request to reset your password. Click the button below to\n        choose a new password.\n      </Text>\n\n      <Section style={buttonContainer}>\n        <Button href={resetUrl} style={button}>\n          Reset Password\n        </Button>\n      </Section>\n\n      <Text style={paragraph}>\n        Or copy and paste this URL into your browser:\n      </Text>\n\n      <Text style={linkText}>{resetUrl}</Text>\n\n      {/* NOTE: 1-hour expiration balances security vs user convenience */}\n      <Text style={paragraph}>\n        <strong>This password reset link will expire in 1 hour</strong> for\n        security reasons.\n      </Text>\n\n      <Text style={paragraph}>\n        If you didn't request a password reset, you can safely ignore this\n        email. Your password will remain unchanged.\n      </Text>\n\n      <Text style={securityNote}>\n        <strong>Security tip:</strong> Never share this reset link with anyone.\n        Our support team will never ask for your password or login credentials.\n      </Text>\n    </BaseTemplate>\n  );\n}\n\nconst heading = {\n  fontSize: \"24px\",\n  fontWeight: \"600\",\n  color: colors.text,\n  margin: \"0 0 24px\",\n};\n\nconst paragraph = {\n  fontSize: \"16px\",\n  lineHeight: \"24px\",\n  color: colors.textMuted,\n  margin: \"0 0 16px\",\n};\n\nconst buttonContainer = {\n  textAlign: \"center\" as const,\n  margin: \"32px 0\",\n};\n\n// Uses danger color (red) to emphasize the security-sensitive nature of password resets\nconst button = {\n  backgroundColor: colors.danger,\n  borderRadius: \"6px\",\n  color: colors.white,\n  fontSize: \"16px\",\n  fontWeight: \"600\",\n  textDecoration: \"none\",\n  textAlign: \"center\" as const,\n  display: \"inline-block\",\n  padding: \"12px 24px\",\n  lineHeight: \"20px\",\n};\n\nconst linkText = {\n  fontSize: \"14px\",\n  color: colors.textLight,\n  wordBreak: \"break-all\" as const,\n  margin: \"0 0 16px\",\n  padding: \"12px\",\n  backgroundColor: \"#f8f9fa\",\n  borderRadius: \"4px\",\n  border: \"1px solid #e9ecef\",\n};\n\nconst securityNote = {\n  fontSize: \"14px\",\n  lineHeight: \"20px\",\n  color: \"#6c757d\",\n  margin: \"24px 0 0\",\n  padding: \"16px\",\n  backgroundColor: colors.warning,\n  borderRadius: \"4px\",\n  border: `1px solid ${colors.warningBorder}`,\n};\n"
  },
  {
    "path": "apps/email/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"dist\", \"node_modules\", \".email\"]\n}\n"
  },
  {
    "path": "apps/email/utils/render.ts",
    "content": "import { render } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\n/**\n * Render a React email component to HTML string\n * @param component React email component\n * @returns HTML string\n */\nexport async function renderEmailToHtml(\n  component: ReactElement,\n): Promise<string> {\n  return await render(component, { pretty: true });\n}\n\n/**\n * Render a React email component to plain text string\n * @param component React email component\n * @returns Plain text string\n */\nexport async function renderEmailToText(\n  component: ReactElement,\n): Promise<string> {\n  return await render(component, { plainText: true });\n}\n"
  },
  {
    "path": "apps/web/README.md",
    "content": "# Edge Router\n\nAstro-based edge worker that routes traffic to the app and API workers via Cloudflare service bindings.\n\n[Documentation](https://reactstarter.com/architecture/edge) | [Deployment](https://reactstarter.com/deployment/)\n\n## Development\n\n```bash\nbun web:dev       # Start dev server (http://localhost:4321)\nbun web:build     # Build for production\nbun web:deploy    # Deploy to Cloudflare Workers\n```\n\n## Routing\n\n- `/api/*` → API worker\n- App routes → App worker\n- Static assets served directly from the edge\n"
  },
  {
    "path": "apps/web/_headers",
    "content": "/*\n  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n  X-Content-Type-Options: nosniff\n  Referrer-Policy: strict-origin-when-cross-origin\n  Permissions-Policy: camera=(), microphone=(), geolocation=()\n  Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'\n  X-Frame-Options: DENY\n\n/*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\n/*.css\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.js\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.woff2\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.woff\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.ttf\n  Cache-Control: public, max-age=31536000, immutable\n\n/*.svg\n  Cache-Control: public, max-age=86400\n\n/*.jpg\n  Cache-Control: public, max-age=86400\n\n/*.jpeg\n  Cache-Control: public, max-age=86400\n\n/*.png\n  Cache-Control: public, max-age=86400\n\n/*.webp\n  Cache-Control: public, max-age=86400\n\n/*.ico\n  Cache-Control: public, max-age=86400\n"
  },
  {
    "path": "apps/web/astro.config.mjs",
    "content": "import react from \"@astrojs/react\";\nimport { defineConfig } from \"astro/config\";\nimport { loadEnv } from \"vite\";\n\n// Load root .env variables for the Astro build process (side-effect: populates process.env)\nloadEnv(process.env.NODE_ENV || \"development\", \"../..\", \"\");\n\nexport default defineConfig({\n  site: process.env.PUBLIC_APP_ORIGIN,\n  srcDir: \".\",\n  publicDir: \"./public\",\n  outDir: \"./dist\",\n  output: \"static\",\n  integrations: [react()],\n});\n"
  },
  {
    "path": "apps/web/layouts/BaseLayout.astro",
    "content": "---\nimport '@/styles/globals.css';\n\nexport interface Props {\n  title?: string;\n  description?: string;\n  image?: string;\n}\n\nconst { \n  title = 'React Starter Kit - Modern Full-Stack Web Application', \n  description = 'Modern full-stack web application template optimized for serverless deployment to CDN edge locations. Built with React, TypeScript, and the latest web technologies.',\n  image = '/og-image.png'\n} = Astro.props;\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n    <meta name=\"generator\" content={Astro.generator} />\n    \n    <!-- Primary Meta Tags -->\n    <title>{title}</title>\n    <meta name=\"title\" content={title} />\n    <meta name=\"description\" content={description} />\n    \n    <!-- Open Graph / Facebook -->\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content={new URL(Astro.url.pathname, Astro.site ?? Astro.url)} />\n    <meta property=\"og:title\" content={title} />\n    <meta property=\"og:description\" content={description} />\n    <meta property=\"og:image\" content={new URL(image, Astro.site ?? Astro.url)} />\n\n    <!-- Twitter -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:title\" content={title} />\n    <meta name=\"twitter:description\" content={description} />\n    <meta name=\"twitter:image\" content={new URL(image, Astro.site ?? Astro.url)} />\n  </head>\n  <body>\n    <div class=\"min-h-screen flex flex-col\">\n      <!-- Header -->\n      <header class=\"border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\n        <div class=\"container mx-auto px-4 sm:px-6 lg:px-8\">\n          <div class=\"flex h-16 items-center justify-between\">\n            <div class=\"flex items-center space-x-4\">\n              <a href=\"/\" class=\"text-xl font-bold text-primary\">\n                React Starter Kit\n              </a>\n            </div>\n            <nav class=\"hidden md:flex items-center space-x-6\">\n              <a href=\"/\" class=\"text-sm font-medium text-muted-foreground hover:text-foreground transition-colors\">\n                Home\n              </a>\n              <a href=\"/features\" class=\"text-sm font-medium text-muted-foreground hover:text-foreground transition-colors\">\n                Features\n              </a>\n              <a href=\"/pricing\" class=\"text-sm font-medium text-muted-foreground hover:text-foreground transition-colors\">\n                Pricing\n              </a>\n              <a href=\"/about\" class=\"text-sm font-medium text-muted-foreground hover:text-foreground transition-colors\">\n                About\n              </a>\n            </nav>\n            <div class=\"flex items-center space-x-2\">\n              <a \n                href=\"https://github.com/kriasoft/react-starter-kit\" \n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                class=\"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 rounded-md px-3\"\n              >\n                GitHub\n              </a>\n              <a \n                href=\"https://github.com/kriasoft/react-starter-kit\"\n                class=\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 px-3\"\n              >\n                Get Started\n              </a>\n            </div>\n          </div>\n        </div>\n      </header>\n\n      <!-- Main Content -->\n      <main class=\"flex-1\">\n        <slot />\n      </main>\n\n      <!-- Footer -->\n      <footer class=\"border-t bg-background/95\">\n        <div class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-8\">\n          <div class=\"grid grid-cols-1 md:grid-cols-4 gap-8\">\n            <div class=\"space-y-3\">\n              <h3 class=\"font-semibold\">React Starter Kit</h3>\n              <p class=\"text-sm text-muted-foreground\">\n                Modern full-stack web application template optimized for\n                serverless deployment.\n              </p>\n            </div>\n            <div class=\"space-y-3\">\n              <h4 class=\"font-medium\">Resources</h4>\n              <ul class=\"space-y-2 text-sm text-muted-foreground\">\n                <li>\n                  <a href=\"https://github.com/kriasoft/react-starter-kit#readme\" class=\"hover:text-foreground transition-colors\">\n                    Documentation\n                  </a>\n                </li>\n                <li>\n                  <a href=\"/features\" class=\"hover:text-foreground transition-colors\">\n                    Features\n                  </a>\n                </li>\n                <li>\n                  <a href=\"/pricing\" class=\"hover:text-foreground transition-colors\">\n                    Pricing\n                  </a>\n                </li>\n              </ul>\n            </div>\n            <div class=\"space-y-3\">\n              <h4 class=\"font-medium\">Community</h4>\n              <ul class=\"space-y-2 text-sm text-muted-foreground\">\n                <li>\n                  <a href=\"https://github.com/kriasoft/react-starter-kit\" class=\"hover:text-foreground transition-colors\">\n                    GitHub\n                  </a>\n                </li>\n                <li>\n                  <a href=\"https://discord.gg/kriasoft\" class=\"hover:text-foreground transition-colors\">\n                    Discord\n                  </a>\n                </li>\n                <li>\n                  <a href=\"https://twitter.com/kriasoft\" class=\"hover:text-foreground transition-colors\">\n                    Twitter\n                  </a>\n                </li>\n              </ul>\n            </div>\n            <div class=\"space-y-3\">\n              <h4 class=\"font-medium\">Legal</h4>\n              <ul class=\"space-y-2 text-sm text-muted-foreground\">\n                <li>\n                  <a href=\"https://github.com/kriasoft/react-starter-kit/blob/main/LICENSE\" class=\"hover:text-foreground transition-colors\">\n                    License\n                  </a>\n                </li>\n              </ul>\n            </div>\n          </div>\n          <div class=\"border-t mt-6 pt-6\">\n            <div class=\"flex flex-col sm:flex-row justify-between items-center\">\n              <p class=\"text-sm text-muted-foreground\">\n                © {new Date().getFullYear()} Kriasoft. All rights reserved.\n              </p>\n              <p class=\"text-sm text-muted-foreground\">\n                Built with Astro, React, TypeScript, and Tailwind CSS\n              </p>\n            </div>\n          </div>\n        </div>\n      </footer>\n    </div>\n  </body>\n</html>"
  },
  {
    "path": "apps/web/lib/utils.ts",
    "content": "// Re-export utils from the UI package\nexport * from \"@repo/ui/lib/utils\";\n"
  },
  {
    "path": "apps/web/package.json",
    "content": "{\n  \"name\": \"@repo/web\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"check\": \"astro check\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"deploy\": \"wrangler deploy --env-file ../../.env --env-file ../../.env.local\",\n    \"logs\": \"wrangler tail --env-file ../../.env --env-file ../../.env.local\"\n  },\n  \"dependencies\": {\n    \"@astrojs/react\": \"^4.4.2\",\n    \"@repo/ui\": \"workspace:*\",\n    \"astro\": \"^5.17.2\",\n    \"hono\": \"^4.11.10\",\n    \"react-dom\": \"^19.2.4\",\n    \"react\": \"^19.2.4\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20260218.0\",\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@tailwindcss/postcss\": \"^4.2.0\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@types/react\": \"^19.2.14\",\n    \"postcss\": \"^8.5.6\",\n    \"tailwindcss\": \"^4.2.0\",\n    \"typescript\": \"~5.9.3\",\n    \"wrangler\": \"^4.66.0\"\n  }\n}\n"
  },
  {
    "path": "apps/web/pages/about.astro",
    "content": "---\nimport BaseLayout from '@/layouts/BaseLayout.astro';\nimport { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Separator } from '@repo/ui';\n\nconst title = \"About - React Starter Kit\";\nconst description = \"Learn about React Starter Kit, a production-ready full-stack web application template built with modern technologies.\";\n---\n\n<BaseLayout title={title} description={description}>\n  <div class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-20\">\n    <!-- Hero Section -->\n    <div class=\"text-center mb-16\">\n      <h1 class=\"text-4xl font-bold tracking-tight mb-6\">\n        About React Starter Kit\n      </h1>\n      <p class=\"text-xl text-muted-foreground max-w-3xl mx-auto\">\n        A production-ready, full-stack web application template that combines\n        modern development practices with cutting-edge technologies to deliver\n        exceptional performance and developer experience.\n      </p>\n    </div>\n\n    <!-- Mission Section -->\n    <section class=\"mb-20\">\n      <Card>\n        <CardHeader>\n          <CardTitle className=\"text-2xl\">Our Mission</CardTitle>\n          <CardDescription>\n            Empowering developers to build faster, better web applications\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          <p class=\"text-muted-foreground\">\n            React Starter Kit was created to bridge the gap between prototype\n            and production. We believe that developers should focus on\n            building great features, not wrestling with configuration and\n            setup.\n          </p>\n          <p class=\"text-muted-foreground\">\n            Our template provides a solid foundation with best practices,\n            modern tooling, and optimized performance out of the box, so you\n            can ship your ideas faster and with confidence.\n          </p>\n        </CardContent>\n      </Card>\n    </section>\n\n    <!-- Key Features -->\n    <section class=\"mb-20\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        What Makes Us Different\n      </h2>\n\n      <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n        <Card>\n          <CardHeader>\n            <CardTitle>🎯 Production-Ready</CardTitle>\n            <CardDescription>\n              Not just a demo, but a real foundation for your applications\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Every component, pattern, and configuration has been\n              battle-tested in production environments. Security, performance,\n              and maintainability are built-in from day one.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle>⚡ Edge-First Architecture</CardTitle>\n            <CardDescription>\n              Optimized for global performance at CDN edge locations\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Built specifically for Cloudflare Workers and edge computing.\n              Your applications run closer to your users for lightning-fast\n              response times.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle>🔧 Developer Experience</CardTitle>\n            <CardDescription>\n              Carefully crafted tooling for maximum productivity\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Hot reload, TypeScript support, comprehensive testing setup, and\n              intuitive project structure. Everything you need to stay in the\n              flow.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle>🌐 Full-Stack Solution</CardTitle>\n            <CardDescription>\n              Complete backend and frontend in one cohesive package\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              tRPC for type-safe APIs, Better Auth for authentication and\n              database, and WebSocket support for real-time features.\n            </p>\n          </CardContent>\n        </Card>\n      </div>\n    </section>\n\n    <!-- Technology Choices -->\n    <section class=\"mb-20\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        Technology Choices\n      </h2>\n\n      <Card>\n        <CardContent className=\"pt-6\">\n          <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n            <div>\n              <h3 class=\"font-semibold mb-4\">Frontend Stack</h3>\n              <ul class=\"space-y-2 text-sm text-muted-foreground\">\n                <li>\n                  <strong>React 19:</strong> Latest React with concurrent\n                  features\n                </li>\n                <li>\n                  <strong>TypeScript:</strong> Type safety and better\n                  developer experience\n                </li>\n                <li>\n                  <strong>Astro:</strong> Lightning-fast static site generation\n                </li>\n                <li>\n                  <strong>TanStack Router:</strong> Type-safe routing with\n                  code splitting\n                </li>\n                <li>\n                  <strong>ShadCN UI:</strong> Beautiful, accessible component\n                  library\n                </li>\n                <li>\n                  <strong>Tailwind CSS:</strong> Utility-first CSS framework\n                </li>\n              </ul>\n            </div>\n\n            <div>\n              <h3 class=\"font-semibold mb-4\">Backend Stack</h3>\n              <ul class=\"space-y-2 text-sm text-muted-foreground\">\n                <li>\n                  <strong>Bun:</strong> Fast JavaScript runtime and package\n                  manager\n                </li>\n                <li>\n                  <strong>Hono:</strong> Ultra-fast web framework for edge\n                  computing\n                </li>\n                <li>\n                  <strong>tRPC:</strong> End-to-end type safety for APIs\n                </li>\n                <li>\n                  <strong>Better Auth:</strong> Authentication\n                </li>\n                <li>\n                  <strong>Cloudflare Workers:</strong> Serverless edge\n                  computing\n                </li>\n                <li>\n                  <strong>WebSockets:</strong> Real-time communication support\n                </li>\n              </ul>\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    </section>\n\n    <!-- Team Section -->\n    <section class=\"mb-20\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        Built by Kriasoft\n      </h2>\n\n      <Card>\n        <CardContent className=\"pt-6 text-center\">\n          <p class=\"text-muted-foreground mb-6\">\n            React Starter Kit is maintained by Kriasoft, a team of experienced\n            developers passionate about modern web technologies and developer\n            experience.\n          </p>\n\n          <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n            <Button asChild>\n              <a\n                href=\"https://github.com/kriasoft\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                Visit Kriasoft on GitHub\n              </a>\n            </Button>\n            <Button variant=\"outline\" asChild>\n              <a\n                href=\"https://kriasoft.com\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                Learn More About Kriasoft\n              </a>\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n    </section>\n\n    <Separator className=\"my-12\" />\n\n    <!-- CTA Section -->\n    <section class=\"text-center\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n        Ready to Get Started?\n      </h2>\n      <p class=\"text-muted-foreground text-lg mb-8 max-w-2xl mx-auto\">\n        Join thousands of developers who have chosen React Starter Kit for\n        their next project.\n      </p>\n\n      <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n        <Button size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Get Started Now\n          </a>\n        </Button>\n        <Button variant=\"outline\" size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit/discussions\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Join the Community\n          </a>\n        </Button>\n      </div>\n    </section>\n  </div>\n</BaseLayout>"
  },
  {
    "path": "apps/web/pages/features.astro",
    "content": "---\nimport BaseLayout from '@/layouts/BaseLayout.astro';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/ui';\n\nconst title = \"Features - React Starter Kit\";\nconst description = \"Explore all the powerful features that make React Starter Kit the perfect foundation for your next project.\";\n\nconst featureCategories = [\n  {\n    title: \"Developer Experience\",\n    icon: \"🛠️\",\n    features: [\n      {\n        title: \"TypeScript First\",\n        description: \"Full TypeScript support with strict type checking and IntelliSense\"\n      },\n      {\n        title: \"Hot Module Replacement\",\n        description: \"Instant feedback with fast refresh and state preservation\"\n      },\n      {\n        title: \"Monorepo Structure\",\n        description: \"Organized workspace management with Bun workspaces\"\n      },\n      {\n        title: \"Code Generation\",\n        description: \"Automated route generation and type inference\"\n      }\n    ]\n  },\n  {\n    title: \"Performance\",\n    icon: \"⚡\",\n    features: [\n      {\n        title: \"Edge Rendering\",\n        description: \"Server-side rendering at CDN edge locations worldwide\"\n      },\n      {\n        title: \"Code Splitting\",\n        description: \"Automatic route-based code splitting for optimal loading\"\n      },\n      {\n        title: \"Asset Optimization\",\n        description: \"Image optimization, lazy loading, and efficient bundling\"\n      },\n      {\n        title: \"Zero-Config CDN\",\n        description: \"Built-in CDN support with Cloudflare Workers\"\n      }\n    ]\n  },\n  {\n    title: \"Full-Stack Capabilities\",\n    icon: \"🚀\",\n    features: [\n      {\n        title: \"Type-Safe APIs\",\n        description: \"End-to-end type safety with tRPC\"\n      },\n      {\n        title: \"Authentication\",\n        description: \"Built-in auth with Better Auth and session management\"\n      },\n      {\n        title: \"Database Integration\",\n        description: \"PostgreSQL with Drizzle ORM and migrations\"\n      },\n      {\n        title: \"Real-time Support\",\n        description: \"WebSocket integration for live features\"\n      }\n    ]\n  },\n  {\n    title: \"UI & Design\",\n    icon: \"🎨\",\n    features: [\n      {\n        title: \"Component Library\",\n        description: \"Pre-built components with ShadCN UI\"\n      },\n      {\n        title: \"Tailwind CSS v4\",\n        description: \"Latest Tailwind with automatic class sorting\"\n      },\n      {\n        title: \"Dark Mode\",\n        description: \"Built-in theme support with system preference detection\"\n      },\n      {\n        title: \"Responsive Design\",\n        description: \"Mobile-first approach with adaptive layouts\"\n      }\n    ]\n  },\n  {\n    title: \"Testing & Quality\",\n    icon: \"✅\",\n    features: [\n      {\n        title: \"Unit Testing\",\n        description: \"Vitest setup with coverage reporting\"\n      },\n      {\n        title: \"E2E Testing\",\n        description: \"Playwright integration for browser testing\"\n      },\n      {\n        title: \"Type Checking\",\n        description: \"Strict TypeScript configuration\"\n      },\n      {\n        title: \"Linting & Formatting\",\n        description: \"ESLint and Prettier pre-configured\"\n      }\n    ]\n  },\n  {\n    title: \"Deployment & DevOps\",\n    icon: \"☁️\",\n    features: [\n      {\n        title: \"CI/CD Pipeline\",\n        description: \"GitHub Actions workflow included\"\n      },\n      {\n        title: \"Infrastructure as Code\",\n        description: \"Terraform configuration for Cloudflare\"\n      },\n      {\n        title: \"Environment Management\",\n        description: \"Multi-environment support with .env files\"\n      },\n      {\n        title: \"Monitoring\",\n        description: \"Built-in error tracking and analytics\"\n      }\n    ]\n  }\n];\n---\n\n<BaseLayout title={title} description={description}>\n  <div class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-20\">\n    <!-- Hero Section -->\n    <div class=\"text-center mb-16\">\n      <h1 class=\"text-4xl font-bold tracking-tight mb-6\">\n        Powerful Features for Modern Development\n      </h1>\n      <p class=\"text-xl text-muted-foreground max-w-3xl mx-auto\">\n        Everything you need to build, test, and deploy production-ready web applications\n        with confidence and speed.\n      </p>\n    </div>\n\n    <!-- Feature Categories -->\n    {featureCategories.map((category) => (\n      <section class=\"mb-20\">\n        <div class=\"flex items-center mb-8\">\n          <span class=\"text-4xl mr-4\">{category.icon}</span>\n          <h2 class=\"text-3xl font-bold tracking-tight\">{category.title}</h2>\n        </div>\n        \n        <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n          {category.features.map((feature) => (\n            <Card>\n              <CardHeader>\n                <CardTitle className=\"text-lg\">{feature.title}</CardTitle>\n                <CardDescription>{feature.description}</CardDescription>\n              </CardHeader>\n            </Card>\n          ))}\n        </div>\n      </section>\n    ))}\n\n    <!-- Tech Stack Detail -->\n    <section class=\"mb-20 bg-muted/20 rounded-lg p-12\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        Built with Best-in-Class Technologies\n      </h2>\n      \n      <div class=\"grid grid-cols-2 md:grid-cols-4 gap-6\">\n        {[\n          { name: \"React 19\", category: \"UI Framework\" },\n          { name: \"TypeScript 5.9\", category: \"Language\" },\n          { name: \"Astro\", category: \"Static Site Gen\" },\n          { name: \"Bun\", category: \"Runtime\" },\n          { name: \"Vite\", category: \"Build Tool\" },\n          { name: \"TanStack Router\", category: \"Routing\" },\n          { name: \"tRPC\", category: \"API Layer\" },\n          { name: \"Hono\", category: \"Web Framework\" },\n          { name: \"ShadCN UI\", category: \"Components\" },\n          { name: \"Tailwind CSS\", category: \"Styling\" },\n          { name: \"Drizzle ORM\", category: \"Database\" },\n          { name: \"PostgreSQL\", category: \"Database\" },\n          { name: \"Better Auth\", category: \"Authentication\" },\n          { name: \"Cloudflare\", category: \"Platform\" },\n          { name: \"Vitest\", category: \"Testing\" },\n          { name: \"Terraform\", category: \"Infrastructure\" }\n        ].map((tech) => (\n          <div class=\"bg-background rounded-lg p-4 border\">\n            <div class=\"font-semibold\">{tech.name}</div>\n            <div class=\"text-xs text-muted-foreground\">{tech.category}</div>\n          </div>\n        ))}\n      </div>\n    </section>\n\n    <!-- Code Example -->\n    <section class=\"mb-20\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        Clean, Modern Code\n      </h2>\n      \n      <Card>\n        <CardHeader>\n          <CardTitle>Example: Type-Safe API Route</CardTitle>\n          <CardDescription>\n            Define your API with full type safety from backend to frontend\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <pre class=\"bg-muted p-4 rounded-lg overflow-x-auto\">\n            <code class=\"text-sm\">{`// api/router.ts\nexport const appRouter = router({\n  user: {\n    get: publicProcedure\n      .input(z.object({ id: z.string() }))\n      .query(async ({ input }) => {\n        return await db.user.findUnique({\n          where: { id: input.id }\n        });\n      }),\n  },\n});\n\n// app/page.tsx\nfunction UserProfile({ userId }: Props) {\n  const { data } = trpc.user.get.useQuery({ id: userId });\n  \n  return <div>{data?.name}</div>;\n}`}</code>\n          </pre>\n        </CardContent>\n      </Card>\n    </section>\n\n    <!-- CTA Section -->\n    <section class=\"text-center\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n        Experience the Difference\n      </h2>\n      <p class=\"text-muted-foreground text-lg mb-8 max-w-2xl mx-auto\">\n        See why developers choose React Starter Kit for their most important projects.\n      </p>\n\n      <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n        <a \n          href=\"https://github.com/kriasoft/react-starter-kit\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          class=\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-10 px-4 py-2\"\n        >\n          Try It Now\n        </a>\n        <a \n          href=\"https://github.com/kriasoft/react-starter-kit#readme\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          class=\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2\"\n        >\n          View Documentation\n        </a>\n      </div>\n    </section>\n  </div>\n</BaseLayout>"
  },
  {
    "path": "apps/web/pages/index.astro",
    "content": "---\nimport BaseLayout from '@/layouts/BaseLayout.astro';\nimport { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/ui';\n\nconst features = [\n  {\n    title: \"⚡ Lightning Fast\",\n    description: \"SSR at CDN edge locations with ~100 Lighthouse scores\",\n    content: \"Optimized for Cloudflare Workers with Bun runtime for maximum performance.\"\n  },\n  {\n    title: \"🎨 Modern UI\",\n    description: \"Beautiful components with ShadCN UI and Tailwind CSS\",\n    content: \"Pre-built components following modern design principles and accessibility standards.\"\n  },\n  {\n    title: \"🚀 Developer Experience\",\n    description: \"TypeScript, hot reload, and excellent tooling\",\n    content: \"Full TypeScript support with Vite, TanStack Router, and modern development tools.\"\n  },\n  {\n    title: \"📱 Full-Stack\",\n    description: \"tRPC API with PostgreSQL and authentication\",\n    content: \"Complete backend solution with type-safe APIs and database integration.\"\n  },\n  {\n    title: \"🔧 Configurable\",\n    description: \"Monorepo structure with workspace management\",\n    content: \"Organized codebase with separate app, API, edge, and database workspaces.\"\n  },\n  {\n    title: \"☁️ Serverless Ready\",\n    description: \"Deploy to edge locations worldwide\",\n    content: \"Built for Cloudflare Workers with global distribution and automatic scaling.\"\n  }\n];\n\nconst technologies = [\n  \"React 19\", \"TypeScript\", \"Astro\", \"TanStack Router\", \n  \"ShadCN UI\", \"Tailwind CSS\", \"Bun\", \"Hono\", \n  \"tRPC\", \"PostgreSQL\", \"Cloudflare\", \"Jotai\"\n];\n---\n\n<BaseLayout>\n  <!-- Hero Section -->\n  <section class=\"py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-background to-muted/20\">\n    <div class=\"container mx-auto text-center\">\n      <h1 class=\"text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight mb-6\">\n        React Starter Kit\n      </h1>\n      <p class=\"text-xl text-muted-foreground mb-8 max-w-3xl mx-auto\">\n        Modern full-stack web application template optimized for serverless\n        deployment to CDN edge locations. Built with React, TypeScript, and\n        the latest web technologies.\n      </p>\n      <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n        <Button size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Get Started\n          </a>\n        </Button>\n        <Button variant=\"outline\" size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            View on GitHub\n          </a>\n        </Button>\n      </div>\n    </div>\n  </section>\n\n  <!-- Features Section -->\n  <section class=\"py-20 px-4 sm:px-6 lg:px-8\">\n    <div class=\"container mx-auto\">\n      <div class=\"text-center mb-16\">\n        <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n          Everything you need to build modern web apps\n        </h2>\n        <p class=\"text-muted-foreground text-lg max-w-2xl mx-auto\">\n          React Starter Kit provides a solid foundation with best practices,\n          modern tooling, and optimized performance out of the box.\n        </p>\n      </div>\n\n      <div class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8\">\n        {features.map((feature) => (\n          <Card>\n            <CardHeader>\n              <CardTitle>{feature.title}</CardTitle>\n              <CardDescription>{feature.description}</CardDescription>\n            </CardHeader>\n            <CardContent>\n              <p class=\"text-sm text-muted-foreground\">{feature.content}</p>\n            </CardContent>\n          </Card>\n        ))}\n      </div>\n    </div>\n  </section>\n\n  <!-- Tech Stack Section -->\n  <section class=\"py-20 px-4 sm:px-6 lg:px-8 bg-muted/20\">\n    <div class=\"container mx-auto text-center\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n        Built with Modern Technologies\n      </h2>\n      <p class=\"text-muted-foreground text-lg mb-12 max-w-2xl mx-auto\">\n        Carefully selected technologies that work together seamlessly\n      </p>\n\n      <div class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-8\">\n        {technologies.map((tech) => (\n          <div class=\"p-4 rounded-lg bg-background border\">\n            <span class=\"font-medium\">{tech}</span>\n          </div>\n        ))}\n      </div>\n    </div>\n  </section>\n\n  <!-- CTA Section -->\n  <section class=\"py-20 px-4 sm:px-6 lg:px-8\">\n    <div class=\"container mx-auto text-center\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n        Ready to start building?\n      </h2>\n      <p class=\"text-muted-foreground text-lg mb-8 max-w-2xl mx-auto\">\n        Get started with React Starter Kit today and build your next project\n        with confidence.\n      </p>\n\n      <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n        <Button size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Clone Repository\n          </a>\n        </Button>\n        <Button variant=\"outline\" size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit#readme\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Read Documentation\n          </a>\n        </Button>\n      </div>\n    </div>\n  </section>\n</BaseLayout>"
  },
  {
    "path": "apps/web/pages/pricing.astro",
    "content": "---\nimport BaseLayout from '@/layouts/BaseLayout.astro';\nimport { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/ui';\n\nconst title = \"Pricing - React Starter Kit\";\nconst description = \"React Starter Kit is free and open source. Choose the support level that's right for your team.\";\n\nconst plans = [\n  {\n    name: \"Open Source\",\n    price: \"$0\",\n    period: \"forever\",\n    description: \"Perfect for personal projects and learning\",\n    features: [\n      \"Full source code access\",\n      \"MIT License\",\n      \"Community support\",\n      \"GitHub issues\",\n      \"Regular updates\",\n      \"All features included\"\n    ],\n    cta: \"Get Started\",\n    href: \"https://github.com/kriasoft/react-starter-kit\",\n    variant: \"outline\" as const\n  },\n  {\n    name: \"Professional\",\n    price: \"$299\",\n    period: \"one-time\",\n    description: \"For teams that need priority support\",\n    popular: true,\n    features: [\n      \"Everything in Open Source\",\n      \"Priority email support\",\n      \"Private Discord channel\",\n      \"Code review sessions\",\n      \"Architecture consultation\",\n      \"Custom deployment help\"\n    ],\n    cta: \"Contact Sales\",\n    href: \"mailto:hello@kriasoft.com?subject=React Starter Kit Professional\",\n    variant: \"default\" as const\n  },\n  {\n    name: \"Enterprise\",\n    price: \"Custom\",\n    period: \"contact us\",\n    description: \"For organizations with specific needs\",\n    features: [\n      \"Everything in Professional\",\n      \"SLA guarantees\",\n      \"Custom integrations\",\n      \"Training workshops\",\n      \"Dedicated support team\",\n      \"White-label options\"\n    ],\n    cta: \"Contact Us\",\n    href: \"mailto:hello@kriasoft.com?subject=React Starter Kit Enterprise\",\n    variant: \"outline\" as const\n  }\n];\n---\n\n<BaseLayout title={title} description={description}>\n  <div class=\"container mx-auto px-4 sm:px-6 lg:px-8 py-20\">\n    <!-- Hero Section -->\n    <div class=\"text-center mb-16\">\n      <h1 class=\"text-4xl font-bold tracking-tight mb-6\">\n        Simple, Transparent Pricing\n      </h1>\n      <p class=\"text-xl text-muted-foreground max-w-3xl mx-auto\">\n        React Starter Kit is free and open source. Choose additional support\n        options based on your team's needs.\n      </p>\n    </div>\n\n    <!-- Pricing Cards -->\n    <div class=\"grid grid-cols-1 md:grid-cols-3 gap-8 mb-20\">\n      {plans.map((plan) => (\n        <Card className={plan.popular ? \"border-primary shadow-lg relative\" : \"\"}>\n          {plan.popular && (\n            <div class=\"absolute -top-3 left-1/2 -translate-x-1/2\">\n              <span class=\"bg-primary text-primary-foreground text-xs font-semibold px-3 py-1 rounded-full\">\n                MOST POPULAR\n              </span>\n            </div>\n          )}\n          <CardHeader>\n            <CardTitle className=\"text-2xl\">{plan.name}</CardTitle>\n            <CardDescription>{plan.description}</CardDescription>\n            <div class=\"mt-4\">\n              <span class=\"text-4xl font-bold\">{plan.price}</span>\n              {plan.period && (\n                <span class=\"text-muted-foreground ml-2\">{plan.period}</span>\n              )}\n            </div>\n          </CardHeader>\n          <CardContent>\n            <ul class=\"space-y-3 mb-6\">\n              {plan.features.map((feature) => (\n                <li class=\"flex items-start\">\n                  <svg \n                    class=\"h-5 w-5 text-green-500 mr-2 mt-0.5 flex-shrink-0\" \n                    fill=\"none\" \n                    stroke=\"currentColor\" \n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path \n                      stroke-linecap=\"round\" \n                      stroke-linejoin=\"round\" \n                      stroke-width=\"2\" \n                      d=\"M5 13l4 4L19 7\"\n                    />\n                  </svg>\n                  <span class=\"text-sm\">{feature}</span>\n                </li>\n              ))}\n            </ul>\n            <Button\n              variant={plan.variant}\n              className=\"w-full\"\n              asChild\n            >\n              <a \n                href={plan.href}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                {plan.cta}\n              </a>\n            </Button>\n          </CardContent>\n        </Card>\n      ))}\n    </div>\n\n    <!-- FAQ Section -->\n    <section class=\"mb-20\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-8 text-center\">\n        Frequently Asked Questions\n      </h2>\n\n      <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto\">\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"text-lg\">Is React Starter Kit really free?</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Yes! React Starter Kit is completely free and open source under the MIT license.\n              You can use it for personal or commercial projects without any restrictions.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"text-lg\">What's included in support?</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Professional and Enterprise support includes direct access to our team for\n              technical questions, code reviews, and deployment assistance.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"text-lg\">Can I upgrade later?</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Absolutely! You can start with the open source version and upgrade to\n              Professional or Enterprise support whenever you need it.\n            </p>\n          </CardContent>\n        </Card>\n\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"text-lg\">Do you offer custom development?</CardTitle>\n          </CardHeader>\n          <CardContent>\n            <p class=\"text-sm text-muted-foreground\">\n              Yes, our Enterprise plan includes custom development and integration services.\n              Contact us to discuss your specific requirements.\n            </p>\n          </CardContent>\n        </Card>\n      </div>\n    </section>\n\n    <!-- CTA Section -->\n    <section class=\"text-center bg-muted/20 rounded-lg p-12\">\n      <h2 class=\"text-3xl font-bold tracking-tight mb-4\">\n        Ready to Build Something Amazing?\n      </h2>\n      <p class=\"text-muted-foreground text-lg mb-8 max-w-2xl mx-auto\">\n        Start with our free open source version and upgrade when you need\n        professional support.\n      </p>\n\n      <div class=\"flex flex-col sm:flex-row gap-4 justify-center\">\n        <Button size=\"lg\" asChild>\n          <a\n            href=\"https://github.com/kriasoft/react-starter-kit\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Start Building for Free\n          </a>\n        </Button>\n        <Button variant=\"outline\" size=\"lg\" asChild>\n          <a\n            href=\"mailto:hello@kriasoft.com\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            Contact Sales\n          </a>\n        </Button>\n      </div>\n    </section>\n  </div>\n</BaseLayout>"
  },
  {
    "path": "apps/web/postcss.config.js",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "apps/web/public/robots.txt",
    "content": "# www.robotstxt.org/\n\n# Allow crawling of all content\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "apps/web/public/site.manifest",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \"/?utm_source=homescreen\",\n  \"display\": \"standalone\",\n  \"background_color\": \"#fafafa\",\n  \"theme_color\": \"#fafafa\"\n}\n"
  },
  {
    "path": "apps/web/styles/globals.css",
    "content": "@import \"../tailwind.config.css\";\n\n/**\n * CSS Variables for ShadCN UI Theming\n *\n * These variables define the color scheme for light and dark modes.\n * They are referenced by the UI components and mapped to Tailwind\n * utilities in tailwind.config.css.\n *\n * Using oklch() for better color interpolation and consistency.\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch\n */\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(1 0 0);\n  --card-foreground: oklch(0.145 0 0);\n  --popover: oklch(1 0 0);\n  --popover-foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --secondary: oklch(0.97 0 0);\n  --secondary-foreground: oklch(0.205 0 0);\n  --muted: oklch(0.97 0 0);\n  --muted-foreground: oklch(0.556 0 0);\n  --accent: oklch(0.97 0 0);\n  --accent-foreground: oklch(0.205 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(0.922 0 0);\n  --input: oklch(0.922 0 0);\n  --ring: oklch(0.708 0 0);\n  --chart-1: oklch(0.646 0.222 41.116);\n  --chart-2: oklch(0.6 0.118 184.704);\n  --chart-3: oklch(0.398 0.07 227.392);\n  --chart-4: oklch(0.828 0.189 84.429);\n  --chart-5: oklch(0.769 0.188 70.08);\n  --sidebar: oklch(0.985 0 0);\n  --sidebar-foreground: oklch(0.145 0 0);\n  --sidebar-primary: oklch(0.205 0 0);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.97 0 0);\n  --sidebar-accent-foreground: oklch(0.205 0 0);\n  --sidebar-border: oklch(0.922 0 0);\n  --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  --card: oklch(0.205 0 0);\n  --card-foreground: oklch(0.985 0 0);\n  --popover: oklch(0.205 0 0);\n  --popover-foreground: oklch(0.985 0 0);\n  --primary: oklch(0.922 0 0);\n  --primary-foreground: oklch(0.205 0 0);\n  --secondary: oklch(0.269 0 0);\n  --secondary-foreground: oklch(0.985 0 0);\n  --muted: oklch(0.269 0 0);\n  --muted-foreground: oklch(0.708 0 0);\n  --accent: oklch(0.269 0 0);\n  --accent-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.704 0.191 22.216);\n  --destructive-foreground: oklch(0.985 0 0);\n  --border: oklch(1 0 0 / 10%);\n  --input: oklch(1 0 0 / 15%);\n  --ring: oklch(0.556 0 0);\n  --chart-1: oklch(0.488 0.243 264.376);\n  --chart-2: oklch(0.696 0.17 162.48);\n  --chart-3: oklch(0.769 0.188 70.08);\n  --chart-4: oklch(0.627 0.265 303.9);\n  --chart-5: oklch(0.645 0.246 16.439);\n  --sidebar: oklch(0.205 0 0);\n  --sidebar-foreground: oklch(0.985 0 0);\n  --sidebar-primary: oklch(0.488 0.243 264.376);\n  --sidebar-primary-foreground: oklch(0.985 0 0);\n  --sidebar-accent: oklch(0.269 0 0);\n  --sidebar-accent-foreground: oklch(0.985 0 0);\n  --sidebar-border: oklch(1 0 0 / 10%);\n  --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n  * {\n    @apply border-border outline-ring/50;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "apps/web/tailwind.config.css",
    "content": "/**\n * Tailwind CSS v4 configuration for the web app.\n * @see https://tailwindcss.com/docs/configuration\n */\n\n@import \"tailwindcss\";\n\n/* Content paths for Tailwind to scan */\n@source \"./pages/**/*.{astro,js,ts,jsx,tsx}\";\n@source \"./layouts/**/*.{astro,js,ts,jsx,tsx}\";\n@source \"../../packages/ui/components/**/*.{ts,tsx}\";\n@source \"../../packages/ui/lib/**/*.{ts,tsx}\";\n\n/* Custom dark mode variant */\n@custom-variant dark (&:is(.dark *));\n\n/* Theme configuration */\n@theme inline {\n  /* Border radius values */\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n\n  /* Color mappings for Tailwind utilities */\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n  --color-chart-1: var(--chart-1);\n  --color-chart-2: var(--chart-2);\n  --color-chart-3: var(--chart-3);\n  --color-chart-4: var(--chart-4);\n  --color-chart-5: var(--chart-5);\n  --color-sidebar: var(--sidebar);\n  --color-sidebar-foreground: var(--sidebar-foreground);\n  --color-sidebar-primary: var(--sidebar-primary);\n  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n  --color-sidebar-accent: var(--sidebar-accent);\n  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n  --color-sidebar-border: var(--sidebar-border);\n  --color-sidebar-ring: var(--sidebar-ring);\n}\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"noEmit\": true,\n    \"tsBuildInfoFile\": \"../../.cache/tsconfig/web.tsbuildinfo\",\n    \"baseUrl\": \".\",\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"paths\": {\n      \"@/*\": [\"./*\"],\n      \"@repo/ui\": [\"../../packages/ui\"],\n      \"@repo/ui/*\": [\"../../packages/ui/*\"]\n    },\n    \"types\": [\"astro/client\", \"@cloudflare/workers-types\"]\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.json\", \"**/*.astro\"],\n  \"exclude\": [\"**/dist/**/*\", \"**/node_modules/**/*\"],\n  \"references\": [{ \"path\": \"../../packages/ui\" }]\n}\n"
  },
  {
    "path": "apps/web/worker.ts",
    "content": "/**\n * Edge router for the marketing site.\n *\n * Routes \"/\" based on auth-hint cookie presence:\n * - Cookie present: proxy to app (session validated there)\n * - No cookie: serve marketing site\n *\n * See docs/adr/001-auth-hint-cookie.md\n */\n\nimport { Hono } from \"hono\";\nimport { getCookie } from \"hono/cookie\";\n\ninterface Env {\n  ASSETS: Fetcher;\n  APP_SERVICE: Fetcher;\n  API_SERVICE: Fetcher;\n}\n\nconst app = new Hono<{ Bindings: Env }>();\n\n// API proxy\napp.all(\"/api/*\", (c) => c.env.API_SERVICE.fetch(c.req.raw));\n\n// App routes\napp.all(\"/_app/*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\napp.all(\"/login*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\napp.all(\"/signup*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\napp.all(\"/settings*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\napp.all(\"/analytics*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\napp.all(\"/reports*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\n\n// Home page: route based on auth-hint cookie presence\n// __Host-auth (HTTPS) or auth (HTTP dev) — see docs/adr/001-auth-hint-cookie.md\napp.on([\"GET\", \"HEAD\"], \"/\", async (c) => {\n  const hasAuthHint =\n    getCookie(c, \"__Host-auth\") === \"1\" || getCookie(c, \"auth\") === \"1\";\n\n  const upstream = await (hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS).fetch(\n    c.req.raw,\n  );\n\n  // Prevent caching — response varies by auth state\n  const headers = new Headers(upstream.headers);\n  headers.set(\"Cache-Control\", \"private, no-store\");\n  headers.set(\"Vary\", \"Cookie\");\n\n  return new Response(upstream.body, {\n    status: upstream.status,\n    statusText: upstream.statusText,\n    headers,\n  });\n});\n\n// Marketing pages\napp.all(\"*\", (c) => c.env.ASSETS.fetch(c.req.raw));\n\nexport default app;\n"
  },
  {
    "path": "apps/web/wrangler.jsonc",
    "content": "{\n  \"$schema\": \"../../node_modules/wrangler/config-schema.json\",\n\n  // [METADATA]\n  // Edge router that receives all traffic and routes to api/app via service bindings.\n  \"name\": \"example-web\",\n  \"main\": \"./worker.ts\",\n  \"compatibility_date\": \"2025-08-15\",\n  \"compatibility_flags\": [],\n  \"workers_dev\": false,\n\n  // [ASSETS]\n  // Serves bundled JavaScript, CSS, images, and other static assets.\n  \"assets\": {\n    \"directory\": \"./dist\",\n    \"binding\": \"ASSETS\",\n    // Force worker execution for \"/\" to enable auth-aware routing\n    // See docs/adr/001-auth-hint-cookie.md\n    \"run_worker_first\": [\"/\"]\n  },\n\n  // [SERVICE BINDINGS]\n  // Connect to other workers for dynamic routing.\n  // NOTE: services is non-inheritable — must be specified per environment.\n  // Use naming convention: <worker>-<env> (e.g., example-app-staging)\n  \"services\": [\n    { \"binding\": \"APP_SERVICE\", \"service\": \"example-app\" },\n    { \"binding\": \"API_SERVICE\", \"service\": \"example-api\" }\n  ],\n\n  // [ENV:PRODUCTION]\n  // Command: bun wrangler deploy\n  // prettier-ignore\n  \"routes\": [\n    { \"pattern\": \"example.com/*\", \"zone_name\": \"example.com\" }\n  ],\n  \"vars\": {\n    \"ENVIRONMENT\": \"production\"\n  },\n\n  \"env\": {\n    // [ENV:DEVELOPMENT]\n    // Command: bun wrangler dev\n    \"dev\": {\n      \"vars\": {\n        \"ENVIRONMENT\": \"development\"\n      }\n    },\n\n    // [ENV:STAGING]\n    // Command: bun wrangler deploy --env staging\n    \"staging\": {\n      \"services\": [\n        { \"binding\": \"APP_SERVICE\", \"service\": \"example-app-staging\" },\n        { \"binding\": \"API_SERVICE\", \"service\": \"example-api-staging\" }\n      ],\n      \"vars\": {\n        \"ENVIRONMENT\": \"staging\"\n      },\n      // prettier-ignore\n      \"routes\": [\n        { \"pattern\": \"staging.example.com/*\", \"zone_name\": \"example.com\" }\n      ]\n    },\n\n    // [ENV:PREVIEW]\n    // Command: bun wrangler deploy --env preview\n    \"preview\": {\n      \"services\": [\n        { \"binding\": \"APP_SERVICE\", \"service\": \"example-app-preview\" },\n        { \"binding\": \"API_SERVICE\", \"service\": \"example-api-preview\" }\n      ],\n      \"vars\": {\n        \"ENVIRONMENT\": \"preview\"\n      },\n      // prettier-ignore\n      \"routes\": [\n        { \"pattern\": \"preview.example.com/*\", \"zone_name\": \"example.com\" }\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "db/AGENTS.md",
    "content": "## Schema Conventions\n\n- Drizzle `casing: \"snake_case\"` — use camelCase in TypeScript, columns map to snake_case in DB.\n- All primary keys: `text().primaryKey().$defaultFn(() => generateAuthId(...))` — application-generated prefixed CUID2 IDs (e.g. `usr_ght4k2jxm7pqbv01`). See `db/schema/id.ts` for prefix map.\n- Timestamps: `timestamp({ withTimezone: true, mode: \"date\" })`. Every table has `createdAt` (`.defaultNow().notNull()`) and `updatedAt` (`.defaultNow().$onUpdate(() => new Date()).notNull()`).\n- `identity` table = Better Auth's `account` table, renamed via `account.modelName: \"identity\"` in auth config.\n- `member.role` and `invitation.status` are free `text`, not pgEnum — avoids fragile coupling with Better Auth's values.\n- `organization.metadata` is `text`, not JSONB — Better Auth handles serialization.\n\n## Extended Fields (beyond Better Auth defaults)\n\n- **Passkey:** `lastUsedAt` (security audits), `deviceName` (user-friendly label), `platform` (\"platform\" | \"cross-platform\").\n- **Invitation:** `acceptedAt`/`rejectedAt` lifecycle timestamps.\n- **Member roles:** free text `role` (\"owner\", \"admin\", \"member\") — not pgEnum, to stay compatible with Better Auth's role customization.\n\n## Indexes and Constraints\n\n- Every foreign key column gets an index: `{table}_{column}_idx`.\n- Composite uniques: `member(userId, organizationId)`, `invitation(organizationId, email)`, `identity(providerId, accountId)`.\n- `session.activeOrganizationId` has an index but no FK constraint (Better Auth design).\n- All foreign keys use `onDelete: \"cascade\"`.\n\n## Seeds\n\n- Use `onConflictDoNothing()` for idempotent seeds (safe to rerun).\n\n## Environment\n\n- `ENVIRONMENT` env var overrides `NODE_ENV` for env file selection. DB scripts use short names (`prod`, `staging`, `dev`); API env schema uses full names (`production`, `staging`, `development`).\n- DB scripts have `:staging` / `:prod` variants (e.g., `bun db:push:prod`).\n- Config loads `.env.{envName}.local` → `.env.local` → `.env` in priority order.\n"
  },
  {
    "path": "db/README.md",
    "content": "# Database Layer\n\nDatabase layer using [Drizzle ORM](https://orm.drizzle.team/) and PostgreSQL ([Neon](https://neon.tech/)) via Cloudflare Hyperdrive.\n\n[Documentation](https://reactstarter.com/database/) | [Schema](https://reactstarter.com/database/schema) | [Migrations](https://reactstarter.com/database/migrations)\n\n## Structure\n\n```bash\ndb/\n├── schema/             # Table definitions and relations\n├── migrations/         # Auto-generated migration files\n├── seeds/              # Seed scripts (e.g., users)\n├── scripts/            # DB utilities (seed/export)\n├── drizzle.config.ts   # Drizzle configuration\n└── package.json        # DB-only scripts and deps\n```\n\n## Environment\n\n- `DATABASE_URL` is required and loaded from repo root: `.env.<environment>.local` → `.env.local` → `.env`.\n- Environment selection: `ENVIRONMENT` takes priority, otherwise `NODE_ENV=production|staging|test` falls back to `prod|staging|test`; default is `dev`.\n\nExample `.env.dev.local` (at repo root):\n\n```txt\nDATABASE_URL=postgresql://user:password@host:5432/database\n```\n\n## Commands\n\nFrom the repo root:\n\n```bash\nbun db:push       # Apply schema (drizzle-kit push)\nbun db:generate   # Generate migration from schema changes\nbun db:migrate    # Run pending migrations\nbun db:studio     # Open Drizzle Studio\nbun db:seed       # Run seed scripts\nbun db:check      # Drift check\n```\n\nAppend `:staging` or `:prod` to target other environments:\n\n```bash\nbun db:push:staging        # Uses .env.staging.local → .env.local → .env\nbun db:push:prod           # Uses .env.prod.local   → .env.local → .env\nbun db:seed:prod\nbun db:studio:prod\n```\n\n## Typical Workflow\n\n1. Update schema in `db/schema`.\n2. Generate a migration: `bun db:generate --name <migration-name>`.\n3. Apply locally: `bun db:migrate` (or `db:push` for schema sync).\n4. Validate in Drizzle Studio: `bun db:studio`.\n5. Apply to staging/prod with the matching `:staging` or `:prod` suffix.\n\n## Importing Schemas\n\n```typescript\nimport { schema } from \"@repo/db\";\nimport { user } from \"@repo/db/schema/user\";\nimport { organization, member } from \"@repo/db/schema/organization\";\n```\n\n## ID Generation\n\nAll primary keys use application-generated prefixed CUID2 IDs (e.g. `usr_ght4k2jxm7pqbv01`). IDs are generated at the application level via `$defaultFn()` -- no database-level defaults. See `db/schema/id.ts` for the prefix map.\n"
  },
  {
    "path": "db/backups/.gitignore",
    "content": "# Ignore all backup files\n*.sql\n\n# Keep the directory in git\n!.gitignore\n"
  },
  {
    "path": "db/drizzle.config.ts",
    "content": "import { configDotenv } from \"dotenv\";\nimport { defineConfig } from \"drizzle-kit\";\nimport { resolve } from \"node:path\";\n\n// Environment detection: ENVIRONMENT var takes priority, then NODE_ENV mapping\nconst envName = (() => {\n  if (process.env.ENVIRONMENT) return process.env.ENVIRONMENT;\n  if (process.env.NODE_ENV === \"production\") return \"prod\";\n  if (process.env.NODE_ENV === \"staging\") return \"staging\";\n  if (process.env.NODE_ENV === \"test\") return \"test\";\n  return \"dev\";\n})();\n\n// Load .env files in priority order: environment-specific → local → base\nfor (const file of [`.env.${envName}.local`, \".env.local\", \".env\"]) {\n  configDotenv({ path: resolve(__dirname, \"..\", file), quiet: true });\n}\n\nif (!process.env.DATABASE_URL) {\n  throw new Error(\"DATABASE_URL environment variable is required\");\n}\n\n// Validate DATABASE_URL format (accepts both postgres:// and postgresql://)\nif (!/^postgre(s|sql):\\/\\/.+/.test(process.env.DATABASE_URL)) {\n  throw new Error(\"DATABASE_URL must be a valid PostgreSQL connection string\");\n}\n\n/**\n * Drizzle ORM configuration for Neon PostgreSQL database\n *\n * @see https://orm.drizzle.team/docs/drizzle-config-file\n * @see https://orm.drizzle.team/llms.txt\n */\nexport default defineConfig({\n  out: \"./migrations\",\n  schema: \"./schema\",\n  dialect: \"postgresql\",\n  casing: \"snake_case\",\n  dbCredentials: {\n    url: process.env.DATABASE_URL,\n  },\n});\n"
  },
  {
    "path": "db/index.ts",
    "content": "/**\n * @file Database schema exports.\n *\n * Re-exports Drizzle ORM schemas for users, organizations, and authentication.\n */\n\nimport * as schema from \"./schema\";\n\nexport * from \"./schema\";\nexport { schema };\nexport type DatabaseSchema = typeof schema;\n"
  },
  {
    "path": "db/migrations/0000_init.sql",
    "content": "CREATE TABLE \"invitation\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"email\" text NOT NULL,\n\t\"inviter_id\" text NOT NULL,\n\t\"organization_id\" text NOT NULL,\n\t\"role\" text NOT NULL,\n\t\"status\" text DEFAULT 'pending' NOT NULL,\n\t\"expires_at\" timestamp with time zone NOT NULL,\n\t\"accepted_at\" timestamp with time zone,\n\t\"rejected_at\" timestamp with time zone,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"invitation_org_email_unique\" UNIQUE(\"organization_id\",\"email\")\n);\n--> statement-breakpoint\nCREATE TABLE \"passkey\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text,\n\t\"public_key\" text NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"credential_id\" text NOT NULL,\n\t\"counter\" integer DEFAULT 0 NOT NULL,\n\t\"device_type\" text NOT NULL,\n\t\"backed_up\" boolean NOT NULL,\n\t\"transports\" text,\n\t\"aaguid\" text,\n\t\"last_used_at\" timestamp with time zone,\n\t\"device_name\" text,\n\t\"platform\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"passkey_credentialID_unique\" UNIQUE(\"credential_id\")\n);\n--> statement-breakpoint\nCREATE TABLE \"member\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"organization_id\" text NOT NULL,\n\t\"role\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"member_user_org_unique\" UNIQUE(\"user_id\",\"organization_id\")\n);\n--> statement-breakpoint\nCREATE TABLE \"organization\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"slug\" text NOT NULL,\n\t\"logo\" text,\n\t\"metadata\" text,\n\t\"stripe_customer_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"organization_slug_unique\" UNIQUE(\"slug\")\n);\n--> statement-breakpoint\nCREATE TABLE \"identity\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"account_id\" text NOT NULL,\n\t\"provider_id\" text NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"access_token\" text,\n\t\"refresh_token\" text,\n\t\"id_token\" text,\n\t\"access_token_expires_at\" timestamp with time zone,\n\t\"refresh_token_expires_at\" timestamp with time zone,\n\t\"scope\" text,\n\t\"password\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"identity_provider_account_unique\" UNIQUE(\"provider_id\",\"account_id\")\n);\n--> statement-breakpoint\nCREATE TABLE \"session\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"expires_at\" timestamp with time zone NOT NULL,\n\t\"token\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"ip_address\" text,\n\t\"user_agent\" text,\n\t\"user_id\" text NOT NULL,\n\t\"active_organization_id\" text,\n\tCONSTRAINT \"session_token_unique\" UNIQUE(\"token\")\n);\n--> statement-breakpoint\nCREATE TABLE \"subscription\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"plan\" text NOT NULL,\n\t\"reference_id\" text NOT NULL,\n\t\"stripe_customer_id\" text,\n\t\"stripe_subscription_id\" text,\n\t\"status\" text DEFAULT 'incomplete' NOT NULL,\n\t\"period_start\" timestamp with time zone,\n\t\"period_end\" timestamp with time zone,\n\t\"trial_start\" timestamp with time zone,\n\t\"trial_end\" timestamp with time zone,\n\t\"cancel_at_period_end\" boolean DEFAULT false,\n\t\"cancel_at\" timestamp with time zone,\n\t\"canceled_at\" timestamp with time zone,\n\t\"ended_at\" timestamp with time zone,\n\t\"seats\" integer,\n\t\"billing_interval\" text,\n\t\"group_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"subscription_stripeSubscriptionId_unique\" UNIQUE(\"stripe_subscription_id\")\n);\n--> statement-breakpoint\nCREATE TABLE \"user\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"email\" text NOT NULL,\n\t\"email_verified\" boolean DEFAULT false NOT NULL,\n\t\"image\" text,\n\t\"is_anonymous\" boolean DEFAULT false NOT NULL,\n\t\"stripe_customer_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"user_email_unique\" UNIQUE(\"email\")\n);\n--> statement-breakpoint\nCREATE TABLE \"verification\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"value\" text NOT NULL,\n\t\"expires_at\" timestamp with time zone NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"verification_identifier_value_unique\" UNIQUE(\"identifier\",\"value\")\n);\n--> statement-breakpoint\nALTER TABLE \"invitation\" ADD CONSTRAINT \"invitation_inviter_id_user_id_fk\" FOREIGN KEY (\"inviter_id\") REFERENCES \"public\".\"user\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"invitation\" ADD CONSTRAINT \"invitation_organization_id_organization_id_fk\" FOREIGN KEY (\"organization_id\") REFERENCES \"public\".\"organization\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"passkey\" ADD CONSTRAINT \"passkey_user_id_user_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"user\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"member\" ADD CONSTRAINT \"member_user_id_user_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"user\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"member\" ADD CONSTRAINT \"member_organization_id_organization_id_fk\" FOREIGN KEY (\"organization_id\") REFERENCES \"public\".\"organization\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"identity\" ADD CONSTRAINT \"identity_user_id_user_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"user\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nALTER TABLE \"session\" ADD CONSTRAINT \"session_user_id_user_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"user\"(\"id\") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint\nCREATE INDEX \"invitation_email_idx\" ON \"invitation\" USING btree (\"email\");--> statement-breakpoint\nCREATE INDEX \"invitation_inviter_id_idx\" ON \"invitation\" USING btree (\"inviter_id\");--> statement-breakpoint\nCREATE INDEX \"invitation_organization_id_idx\" ON \"invitation\" USING btree (\"organization_id\");--> statement-breakpoint\nCREATE INDEX \"passkey_user_id_idx\" ON \"passkey\" USING btree (\"user_id\");--> statement-breakpoint\nCREATE INDEX \"member_user_id_idx\" ON \"member\" USING btree (\"user_id\");--> statement-breakpoint\nCREATE INDEX \"member_organization_id_idx\" ON \"member\" USING btree (\"organization_id\");--> statement-breakpoint\nCREATE INDEX \"identity_user_id_idx\" ON \"identity\" USING btree (\"user_id\");--> statement-breakpoint\nCREATE INDEX \"session_user_id_idx\" ON \"session\" USING btree (\"user_id\");--> statement-breakpoint\nCREATE INDEX \"session_active_org_id_idx\" ON \"session\" USING btree (\"active_organization_id\");--> statement-breakpoint\nCREATE INDEX \"subscription_reference_id_idx\" ON \"subscription\" USING btree (\"reference_id\");--> statement-breakpoint\nCREATE INDEX \"subscription_stripe_customer_id_idx\" ON \"subscription\" USING btree (\"stripe_customer_id\");--> statement-breakpoint\nCREATE INDEX \"verification_identifier_idx\" ON \"verification\" USING btree (\"identifier\");--> statement-breakpoint\nCREATE INDEX \"verification_value_idx\" ON \"verification\" USING btree (\"value\");--> statement-breakpoint\nCREATE INDEX \"verification_expires_at_idx\" ON \"verification\" USING btree (\"expires_at\");"
  },
  {
    "path": "db/migrations/meta/0000_snapshot.json",
    "content": "{\n  \"id\": \"2f162304-a16e-4ba9-bf5b-dac3c1e4f6c0\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\",\n  \"version\": \"1\",\n  \"dialect\": \"postgresql\",\n  \"tables\": {\n    \"public.invitation\": {\n      \"name\": \"invitation\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"inviter_id\": {\n          \"name\": \"inviter_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"organization_id\": {\n          \"name\": \"organization_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"'pending'\"\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"accepted_at\": {\n          \"name\": \"accepted_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"rejected_at\": {\n          \"name\": \"rejected_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"invitation_email_idx\": {\n          \"name\": \"invitation_email_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"email\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"invitation_inviter_id_idx\": {\n          \"name\": \"invitation_inviter_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"inviter_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"invitation_organization_id_idx\": {\n          \"name\": \"invitation_organization_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"organization_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {\n        \"invitation_inviter_id_user_id_fk\": {\n          \"name\": \"invitation_inviter_id_user_id_fk\",\n          \"tableFrom\": \"invitation\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"inviter_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"invitation_organization_id_organization_id_fk\": {\n          \"name\": \"invitation_organization_id_organization_id_fk\",\n          \"tableFrom\": \"invitation\",\n          \"tableTo\": \"organization\",\n          \"columnsFrom\": [\"organization_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"invitation_org_email_unique\": {\n          \"name\": \"invitation_org_email_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"organization_id\", \"email\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.passkey\": {\n      \"name\": \"passkey\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"public_key\": {\n          \"name\": \"public_key\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"credential_id\": {\n          \"name\": \"credential_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"counter\": {\n          \"name\": \"counter\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": 0\n        },\n        \"device_type\": {\n          \"name\": \"device_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"backed_up\": {\n          \"name\": \"backed_up\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"transports\": {\n          \"name\": \"transports\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"aaguid\": {\n          \"name\": \"aaguid\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"last_used_at\": {\n          \"name\": \"last_used_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"device_name\": {\n          \"name\": \"device_name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"platform\": {\n          \"name\": \"platform\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"passkey_user_id_idx\": {\n          \"name\": \"passkey_user_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"user_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {\n        \"passkey_user_id_user_id_fk\": {\n          \"name\": \"passkey_user_id_user_id_fk\",\n          \"tableFrom\": \"passkey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"passkey_credentialID_unique\": {\n          \"name\": \"passkey_credentialID_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"credential_id\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.member\": {\n      \"name\": \"member\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"organization_id\": {\n          \"name\": \"organization_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"member_user_id_idx\": {\n          \"name\": \"member_user_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"user_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"member_organization_id_idx\": {\n          \"name\": \"member_organization_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"organization_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {\n        \"member_user_id_user_id_fk\": {\n          \"name\": \"member_user_id_user_id_fk\",\n          \"tableFrom\": \"member\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"member_organization_id_organization_id_fk\": {\n          \"name\": \"member_organization_id_organization_id_fk\",\n          \"tableFrom\": \"member\",\n          \"tableTo\": \"organization\",\n          \"columnsFrom\": [\"organization_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"member_user_org_unique\": {\n          \"name\": \"member_user_org_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"user_id\", \"organization_id\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.organization\": {\n      \"name\": \"organization\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"slug\": {\n          \"name\": \"slug\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"logo\": {\n          \"name\": \"logo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"stripe_customer_id\": {\n          \"name\": \"stripe_customer_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"organization_slug_unique\": {\n          \"name\": \"organization_slug_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"slug\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.identity\": {\n      \"name\": \"identity\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"account_id\": {\n          \"name\": \"account_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"provider_id\": {\n          \"name\": \"provider_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"access_token_expires_at\": {\n          \"name\": \"access_token_expires_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"refresh_token_expires_at\": {\n          \"name\": \"refresh_token_expires_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"identity_user_id_idx\": {\n          \"name\": \"identity_user_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"user_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {\n        \"identity_user_id_user_id_fk\": {\n          \"name\": \"identity_user_id_user_id_fk\",\n          \"tableFrom\": \"identity\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"identity_provider_account_unique\": {\n          \"name\": \"identity_provider_account_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"provider_id\", \"account_id\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.session\": {\n      \"name\": \"session\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"ip_address\": {\n          \"name\": \"ip_address\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"user_agent\": {\n          \"name\": \"user_agent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"user_id\": {\n          \"name\": \"user_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"active_organization_id\": {\n          \"name\": \"active_organization_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        }\n      },\n      \"indexes\": {\n        \"session_user_id_idx\": {\n          \"name\": \"session_user_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"user_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"session_active_org_id_idx\": {\n          \"name\": \"session_active_org_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"active_organization_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {\n        \"session_user_id_user_id_fk\": {\n          \"name\": \"session_user_id_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\"user_id\"],\n          \"columnsTo\": [\"id\"],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"session_token_unique\": {\n          \"name\": \"session_token_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"token\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.subscription\": {\n      \"name\": \"subscription\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"plan\": {\n          \"name\": \"plan\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"reference_id\": {\n          \"name\": \"reference_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"stripe_customer_id\": {\n          \"name\": \"stripe_customer_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"stripe_subscription_id\": {\n          \"name\": \"stripe_subscription_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"'incomplete'\"\n        },\n        \"period_start\": {\n          \"name\": \"period_start\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"period_end\": {\n          \"name\": \"period_end\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"trial_start\": {\n          \"name\": \"trial_start\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"trial_end\": {\n          \"name\": \"trial_end\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"cancel_at_period_end\": {\n          \"name\": \"cancel_at_period_end\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"default\": false\n        },\n        \"cancel_at\": {\n          \"name\": \"cancel_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"canceled_at\": {\n          \"name\": \"canceled_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"ended_at\": {\n          \"name\": \"ended_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"seats\": {\n          \"name\": \"seats\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"billing_interval\": {\n          \"name\": \"billing_interval\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"group_id\": {\n          \"name\": \"group_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"subscription_reference_id_idx\": {\n          \"name\": \"subscription_reference_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"reference_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"subscription_stripe_customer_id_idx\": {\n          \"name\": \"subscription_stripe_customer_id_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"stripe_customer_id\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"subscription_stripeSubscriptionId_unique\": {\n          \"name\": \"subscription_stripeSubscriptionId_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"stripe_subscription_id\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.user\": {\n      \"name\": \"user\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"email_verified\": {\n          \"name\": \"email_verified\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"is_anonymous\": {\n          \"name\": \"is_anonymous\",\n          \"type\": \"boolean\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": false\n        },\n        \"stripe_customer_id\": {\n          \"name\": \"stripe_customer_id\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"email\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    },\n    \"public.verification\": {\n      \"name\": \"verification\",\n      \"schema\": \"\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true\n        },\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true\n        },\n        \"created_at\": {\n          \"name\": \"created_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        },\n        \"updated_at\": {\n          \"name\": \"updated_at\",\n          \"type\": \"timestamp with time zone\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"default\": \"now()\"\n        }\n      },\n      \"indexes\": {\n        \"verification_identifier_idx\": {\n          \"name\": \"verification_identifier_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"identifier\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"verification_value_idx\": {\n          \"name\": \"verification_value_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"value\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        },\n        \"verification_expires_at_idx\": {\n          \"name\": \"verification_expires_at_idx\",\n          \"columns\": [\n            {\n              \"expression\": \"expires_at\",\n              \"isExpression\": false,\n              \"asc\": true,\n              \"nulls\": \"last\"\n            }\n          ],\n          \"isUnique\": false,\n          \"concurrently\": false,\n          \"method\": \"btree\",\n          \"with\": {}\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {\n        \"verification_identifier_value_unique\": {\n          \"name\": \"verification_identifier_value_unique\",\n          \"nullsNotDistinct\": false,\n          \"columns\": [\"identifier\", \"value\"]\n        }\n      },\n      \"policies\": {},\n      \"checkConstraints\": {},\n      \"isRLSEnabled\": false\n    }\n  },\n  \"enums\": {},\n  \"schemas\": {},\n  \"sequences\": {},\n  \"roles\": {},\n  \"policies\": {},\n  \"views\": {},\n  \"_meta\": {\n    \"columns\": {},\n    \"schemas\": {},\n    \"tables\": {}\n  }\n}\n"
  },
  {
    "path": "db/migrations/meta/_journal.json",
    "content": "{\n  \"version\": \"7\",\n  \"dialect\": \"postgresql\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"1\",\n      \"when\": 1751197781613,\n      \"tag\": \"0000_init\",\n      \"breakpoints\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "db/package.json",
    "content": "{\n  \"name\": \"@repo/db\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./index.ts\",\n    \"./schema\": \"./schema/index.ts\",\n    \"./schema/*\": \"./schema/*\"\n  },\n  \"scripts\": {\n    \"generate\": \"bun --bun drizzle-kit generate\",\n    \"generate:staging\": \"bun --bun --env ENVIRONMENT=staging drizzle-kit generate\",\n    \"generate:prod\": \"bun --bun --env ENVIRONMENT=prod drizzle-kit generate\",\n    \"migrate\": \"bun --bun drizzle-kit migrate\",\n    \"migrate:staging\": \"bun --bun --env ENVIRONMENT=staging drizzle-kit migrate\",\n    \"migrate:prod\": \"bun --bun --env ENVIRONMENT=prod drizzle-kit migrate\",\n    \"push\": \"bun --bun drizzle-kit push\",\n    \"push:staging\": \"bun --bun --env ENVIRONMENT=staging drizzle-kit push\",\n    \"push:prod\": \"bun --bun --env ENVIRONMENT=prod drizzle-kit push\",\n    \"studio\": \"bun --bun drizzle-kit studio\",\n    \"studio:staging\": \"bun --bun --env ENVIRONMENT=staging drizzle-kit studio\",\n    \"studio:prod\": \"bun --bun --env ENVIRONMENT=prod drizzle-kit studio\",\n    \"seed\": \"bun scripts/seed.ts\",\n    \"seed:staging\": \"bun --env ENVIRONMENT=staging scripts/seed.ts\",\n    \"seed:prod\": \"bun --env ENVIRONMENT=prod scripts/seed.ts\",\n    \"export\": \"bun scripts/export.ts\",\n    \"export:staging\": \"bun --env ENVIRONMENT=staging scripts/export.ts\",\n    \"export:prod\": \"bun --env ENVIRONMENT=prod scripts/export.ts\",\n    \"introspect\": \"bun --bun drizzle-kit introspect\",\n    \"up\": \"bun --bun drizzle-kit up\",\n    \"check\": \"bun --bun drizzle-kit check\",\n    \"drop\": \"bun --bun drizzle-kit drop\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"peerDependencies\": {\n    \"drizzle-orm\": \"^0.45.1\"\n  },\n  \"devDependencies\": {\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/bun\": \"^1.3.9\",\n    \"@types/node\": \"^25.2.3\",\n    \"dotenv\": \"^17.3.1\",\n    \"drizzle-kit\": \"^0.31.9\",\n    \"drizzle-orm\": \"^0.45.1\",\n    \"typescript\": \"~5.9.3\"\n  },\n  \"dependencies\": {\n    \"@paralleldrive/cuid2\": \"^3.3.0\"\n  }\n}\n"
  },
  {
    "path": "db/schema/id.ts",
    "content": "// Prefixed CUID2 ID generation for all database entities.\n// Format: {prefix}_{body} e.g. \"usr_ght4k2jxm7pqbv01\" (20 chars total)\n// See docs/specs/prefixed-ids.md for design rationale.\n\nimport { init } from \"@paralleldrive/cuid2\";\n\n// Keys are Better Auth's internal model names (not table names).\n// \"account\" maps to the \"identity\" table via account.modelName config.\nconst AUTH_PREFIX = {\n  user: \"usr\",\n  session: \"ses\",\n  account: \"idn\", // \"identity\" table — avoids confusion with user/billing account\n  verification: \"vfy\",\n  organization: \"org\",\n  member: \"mem\",\n  invitation: \"inv\",\n  passkey: \"pky\",\n  subscription: \"sub\",\n} as const;\n\nexport type AuthModel = keyof typeof AUTH_PREFIX;\n\nconst ID_LENGTH = 16;\nlet _createId: (() => string) | null = null;\n\nfunction createId(): string {\n  if (!_createId) _createId = init({ length: ID_LENGTH });\n  return _createId();\n}\n\n/** Generate a prefixed ID for a Better Auth model (e.g. `\"user\"` → `\"usr_...\"`) */\nexport function generateAuthId(model: AuthModel): string {\n  const prefix = AUTH_PREFIX[model];\n  if (!prefix) {\n    throw new Error(\n      `Unknown auth model \"${String(model)}\". Add it to AUTH_PREFIX in db/schema/id.ts`,\n    );\n  }\n  return `${prefix}_${createId()}`;\n}\n\n/** Generate a prefixed ID for non-auth tables (e.g. `generateId(\"upl\")`) */\nexport function generateId(prefix: string): string {\n  if (!/^[a-z]{3}$/.test(prefix)) {\n    throw new Error(\n      `ID prefix must be exactly 3 lowercase letters, got \"${prefix}\"`,\n    );\n  }\n  return `${prefix}_${createId()}`;\n}\n"
  },
  {
    "path": "db/schema/index.ts",
    "content": "export * from \"./id\";\nexport * from \"./invitation\";\nexport * from \"./organization\";\nexport * from \"./passkey\";\nexport * from \"./subscription\";\nexport * from \"./user\";\n"
  },
  {
    "path": "db/schema/invitation.ts",
    "content": "// Better Auth invitation system for organization invites\n\nimport { relations } from \"drizzle-orm\";\nimport { index, pgTable, text, timestamp, unique } from \"drizzle-orm/pg-core\";\nimport { generateAuthId } from \"./id\";\nimport { organization } from \"./organization\";\nimport { user } from \"./user\";\n\n/**\n * Invitations table for Better Auth organization plugin.\n * Manages pending invites to organizations.\n *\n * Lifecycle timestamps:\n * - acceptedAt: When the invite was accepted\n * - rejectedAt: When the invite was rejected or canceled\n */\nexport const invitation = pgTable(\n  \"invitation\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"invitation\")),\n    email: text().notNull(),\n    inviterId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    organizationId: text()\n      .notNull()\n      .references(() => organization.id, { onDelete: \"cascade\" }),\n    role: text().notNull(),\n    status: text().default(\"pending\").notNull(),\n    expiresAt: timestamp({ withTimezone: true, mode: \"date\" }).notNull(),\n    acceptedAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    rejectedAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    unique(\"invitation_org_email_unique\").on(table.organizationId, table.email),\n    index(\"invitation_email_idx\").on(table.email),\n    index(\"invitation_inviter_id_idx\").on(table.inviterId),\n    index(\"invitation_organization_id_idx\").on(table.organizationId),\n  ],\n);\n\nexport type Invitation = typeof invitation.$inferSelect;\nexport type NewInvitation = typeof invitation.$inferInsert;\n\n// —————————————————————————————————————————————————————————————————————————————\n// Relations for better query experience\n// —————————————————————————————————————————————————————————————————————————————\n\nexport const invitationRelations = relations(invitation, ({ one }) => ({\n  inviter: one(user, {\n    fields: [invitation.inviterId],\n    references: [user.id],\n  }),\n  organization: one(organization, {\n    fields: [invitation.organizationId],\n    references: [organization.id],\n  }),\n}));\n"
  },
  {
    "path": "db/schema/organization.ts",
    "content": "// Multi-tenant organizations and memberships with role-based access control\n\nimport { relations } from \"drizzle-orm\";\nimport { index, pgTable, text, timestamp, unique } from \"drizzle-orm/pg-core\";\nimport { generateAuthId } from \"./id\";\nimport { user } from \"./user\";\n\n/**\n * Organizations table for Better Auth organization plugin.\n * Each organization represents a separate tenant with isolated data.\n */\nexport const organization = pgTable(\"organization\", {\n  id: text()\n    .primaryKey()\n    .$defaultFn(() => generateAuthId(\"organization\")),\n  name: text().notNull(),\n  slug: text().notNull().unique(),\n  logo: text(),\n  metadata: text(), // Better Auth expects string (JSON serialized)\n  stripeCustomerId: text(),\n  createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .notNull(),\n  updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .$onUpdate(() => new Date())\n    .notNull(),\n});\n\nexport type Organization = typeof organization.$inferSelect;\nexport type NewOrganization = typeof organization.$inferInsert;\n\n/**\n * Organization membership table for Better Auth organization plugin.\n * Links users to organizations with specific roles.\n *\n * Role values (Better Auth defaults):\n * - \"owner\": Full control, can delete organization\n * - \"admin\": Can manage members and settings\n * - \"member\": Standard access\n *\n * @see apps/api/lib/auth.ts creatorRole config\n */\nexport const member = pgTable(\n  \"member\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"member\")),\n    userId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    organizationId: text()\n      .notNull()\n      .references(() => organization.id, { onDelete: \"cascade\" }),\n    role: text().notNull(), // \"owner\" | \"admin\" | \"member\"\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    unique(\"member_user_org_unique\").on(table.userId, table.organizationId),\n    index(\"member_user_id_idx\").on(table.userId),\n    index(\"member_organization_id_idx\").on(table.organizationId),\n  ],\n);\n\nexport type Member = typeof member.$inferSelect;\nexport type NewMember = typeof member.$inferInsert;\n\n// —————————————————————————————————————————————————————————————————————————————\n// Relations for better query experience\n// —————————————————————————————————————————————————————————————————————————————\n\nexport const organizationRelations = relations(organization, ({ many }) => ({\n  members: many(member),\n}));\n\nexport const memberRelations = relations(member, ({ one }) => ({\n  user: one(user, {\n    fields: [member.userId],\n    references: [user.id],\n  }),\n  organization: one(organization, {\n    fields: [member.organizationId],\n    references: [organization.id],\n  }),\n}));\n"
  },
  {
    "path": "db/schema/passkey.ts",
    "content": "// WebAuthn passkey credentials for Better Auth\n// @see https://www.better-auth.com/docs/plugins/passkey\n\nimport {\n  boolean,\n  index,\n  integer,\n  pgTable,\n  text,\n  timestamp,\n} from \"drizzle-orm/pg-core\";\nimport { generateAuthId } from \"./id\";\nimport { user } from \"./user\";\n\n/**\n * Passkey credential store.\n *\n * Extended fields beyond Better Auth defaults:\n * - lastUsedAt: Tracks last authentication for security audits\n * - deviceName: User-friendly name (e.g., \"MacBook Pro\", \"iPhone 15\")\n * - platform: Authenticator platform (\"platform\" | \"cross-platform\")\n */\nexport const passkey = pgTable(\n  \"passkey\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"passkey\")),\n    name: text(),\n    publicKey: text().notNull(),\n    userId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    credentialID: text().notNull().unique(),\n    counter: integer().default(0).notNull(),\n    deviceType: text().notNull(),\n    backedUp: boolean().notNull(),\n    transports: text(),\n    aaguid: text(),\n    // Extended operational fields\n    lastUsedAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    deviceName: text(),\n    platform: text(), // \"platform\" | \"cross-platform\"\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [index(\"passkey_user_id_idx\").on(table.userId)],\n);\n\nexport type Passkey = typeof passkey.$inferSelect;\nexport type NewPasskey = typeof passkey.$inferInsert;\n"
  },
  {
    "path": "db/schema/subscription.ts",
    "content": "// Stripe subscription state managed by the @better-auth/stripe plugin.\n// referenceId is polymorphic: points to user.id or organization.id depending\n// on whether the subscription is personal or org-level billing.\n\nimport {\n  boolean,\n  index,\n  integer,\n  pgTable,\n  text,\n  timestamp,\n} from \"drizzle-orm/pg-core\";\nimport { generateAuthId } from \"./id\";\n\nexport const subscription = pgTable(\n  \"subscription\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"subscription\")),\n    plan: text().notNull(),\n    referenceId: text().notNull(),\n    stripeCustomerId: text(),\n    stripeSubscriptionId: text().unique(),\n    status: text().default(\"incomplete\").notNull(),\n    periodStart: timestamp({ withTimezone: true, mode: \"date\" }),\n    periodEnd: timestamp({ withTimezone: true, mode: \"date\" }),\n    trialStart: timestamp({ withTimezone: true, mode: \"date\" }),\n    trialEnd: timestamp({ withTimezone: true, mode: \"date\" }),\n    cancelAtPeriodEnd: boolean().default(false),\n    cancelAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    canceledAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    endedAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    seats: integer(),\n    billingInterval: text(),\n    groupId: text(),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    index(\"subscription_reference_id_idx\").on(table.referenceId),\n    index(\"subscription_stripe_customer_id_idx\").on(table.stripeCustomerId),\n  ],\n);\n\nexport type Subscription = typeof subscription.$inferSelect;\nexport type NewSubscription = typeof subscription.$inferInsert;\n"
  },
  {
    "path": "db/schema/user.ts",
    "content": "/**\n * Database schema for Better Auth authentication system.\n *\n * This schema is designed to be fully compatible with Better Auth's database\n * requirements as documented at https://www.better-auth.com/docs/concepts/database\n *\n * Tables defined:\n * - `user`: Core user accounts with profile information\n * - `session`: Active user sessions for authentication state\n * - `identity`: OAuth provider accounts (renamed from Better Auth's `account`)\n * - `verification`: Tokens for email verification and password resets\n *\n * @see https://www.better-auth.com/docs/concepts/database\n * @see https://www.better-auth.com/docs/adapters/drizzle\n */\n\nimport { relations } from \"drizzle-orm\";\nimport {\n  boolean,\n  index,\n  pgTable,\n  text,\n  timestamp,\n  unique,\n} from \"drizzle-orm/pg-core\";\nimport { generateAuthId } from \"./id\";\n\n/**\n * User accounts table.\n * Matches to the `user` table in Better Auth.\n */\nexport const user = pgTable(\"user\", {\n  id: text()\n    .primaryKey()\n    .$defaultFn(() => generateAuthId(\"user\")),\n  name: text().notNull(),\n  email: text().notNull().unique(),\n  emailVerified: boolean().default(false).notNull(),\n  image: text(),\n  isAnonymous: boolean().default(false).notNull(),\n  stripeCustomerId: text(),\n  createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .notNull(),\n  updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .$onUpdate(() => new Date())\n    .notNull(),\n});\n\nexport type User = typeof user.$inferSelect;\nexport type NewUser = typeof user.$inferInsert;\n\n/**\n * Stores user session data for authentication.\n * Matches to the `session` table in Better Auth.\n */\nexport const session = pgTable(\n  \"session\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"session\")),\n    expiresAt: timestamp({ withTimezone: true, mode: \"date\" }).notNull(),\n    token: text().notNull().unique(),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n    ipAddress: text(),\n    userAgent: text(),\n    userId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    activeOrganizationId: text(),\n  },\n  (table) => [\n    index(\"session_user_id_idx\").on(table.userId),\n    index(\"session_active_org_id_idx\").on(table.activeOrganizationId),\n  ],\n);\n\nexport type Session = typeof session.$inferSelect;\nexport type NewSession = typeof session.$inferInsert;\n\n/**\n * Stores OAuth provider account information.\n * Matches to the `account` table in Better Auth.\n */\nexport const identity = pgTable(\n  \"identity\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"account\")),\n    accountId: text().notNull(),\n    providerId: text().notNull(),\n    userId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    accessToken: text(),\n    refreshToken: text(),\n    idToken: text(),\n    accessTokenExpiresAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    refreshTokenExpiresAt: timestamp({ withTimezone: true, mode: \"date\" }),\n    scope: text(),\n    password: text(),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    unique(\"identity_provider_account_unique\").on(\n      table.providerId,\n      table.accountId,\n    ),\n    index(\"identity_user_id_idx\").on(table.userId),\n  ],\n);\n\nexport type Identity = typeof identity.$inferSelect;\nexport type NewIdentity = typeof identity.$inferInsert;\n\n/**\n * Stores verification tokens (email verification, password reset, etc.)\n * Matches to the `verification` table in Better Auth.\n */\nexport const verification = pgTable(\n  \"verification\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateAuthId(\"verification\")),\n    identifier: text().notNull(),\n    value: text().notNull(),\n    expiresAt: timestamp({ withTimezone: true, mode: \"date\" }).notNull(),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    unique(\"verification_identifier_value_unique\").on(\n      table.identifier,\n      table.value,\n    ),\n    index(\"verification_identifier_idx\").on(table.identifier),\n    index(\"verification_value_idx\").on(table.value),\n    index(\"verification_expires_at_idx\").on(table.expiresAt),\n  ],\n);\n\nexport type Verification = typeof verification.$inferSelect;\nexport type NewVerification = typeof verification.$inferInsert;\n\n// —————————————————————————————————————————————————————————————————————————————\n// Relations for better query experience\n// —————————————————————————————————————————————————————————————————————————————\n\nexport const userRelations = relations(user, ({ many }) => ({\n  sessions: many(session),\n  identities: many(identity),\n}));\n\nexport const sessionRelations = relations(session, ({ one }) => ({\n  user: one(user, {\n    fields: [session.userId],\n    references: [user.id],\n  }),\n}));\n\nexport const identityRelations = relations(identity, ({ one }) => ({\n  user: one(user, {\n    fields: [identity.userId],\n    references: [user.id],\n  }),\n}));\n"
  },
  {
    "path": "db/scripts/export.ts",
    "content": "#!/usr/bin/env bun\n/**\n * PostgreSQL database export utility with schema/data options\n *\n * Usage:\n *   bun scripts/export.ts                    # Schema only (default)\n *   bun scripts/export.ts --data             # Schema + data\n *   bun scripts/export.ts --data-only        # Data only\n *   bun scripts/export.ts --table=users      # Specific table\n *   bun scripts/export.ts -- --inserts       # Pass pg_dump flags directly\n *\n * Environment:\n *   bun --env ENVIRONMENT=staging scripts/export.ts\n *   bun --env ENVIRONMENT=prod scripts/export.ts\n *\n * REQUIREMENTS:\n * - DATABASE_URL environment variable must be set and valid PostgreSQL connection string\n * - pg_dump binary must be available in PATH (PostgreSQL client tools required, validated at runtime)\n * - ./backups/ directory will be created automatically if it doesn't exist\n * - Output filenames include timestamp, environment, and export type for uniqueness\n * - Process exits with code 1 on any failure for CI/CD integration\n * - File permissions on output SQL files are restricted (readable by owner only)\n * - Script handles concurrent executions without filename conflicts\n */\n\nimport { $ } from \"bun\";\nimport { existsSync } from \"fs\";\nimport { chmod, mkdir } from \"fs/promises\";\nimport { resolve } from \"path\";\n\n// Import drizzle config to trigger environment loading and validation\nimport \"../drizzle.config\";\n\n// Parse arguments\nconst args = process.argv.slice(2);\nconst passThrough: string[] = [];\nlet includeData = false;\nlet dataOnly = false;\nlet table: string | undefined;\n\n// Find pass-through arguments (after --)\nconst dashIndex = args.indexOf(\"--\");\nif (dashIndex !== -1) {\n  passThrough.push(...args.slice(dashIndex + 1));\n  args.splice(dashIndex);\n}\n\n// Parse named arguments\nfor (const arg of args) {\n  if (arg === \"--data\") {\n    includeData = true;\n  } else if (arg === \"--data-only\") {\n    dataOnly = true;\n  } else if (arg.startsWith(\"--table=\")) {\n    table = arg.split(\"=\")[1];\n  }\n}\n\n// Build pg_dump command\nconst pgDumpArgs: string[] = [];\n\n// pg_dump requires the connection string as the last positional argument\n// or through -d/--dbname flag\npgDumpArgs.push(\"--dbname\", process.env.DATABASE_URL!);\n\n// Default options\npgDumpArgs.push(\"--format=plain\", \"--encoding=UTF-8\");\n\n// Handle export type\nif (dataOnly) {\n  pgDumpArgs.push(\"--data-only\");\n} else if (!includeData) {\n  pgDumpArgs.push(\"--schema-only\");\n}\n\n// Handle table selection\nif (table) {\n  pgDumpArgs.push(`--table=${table}`);\n}\n\n// Add pass-through arguments\npgDumpArgs.push(...passThrough);\n\n// Generate filename based on options with high precision timestamp\nconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\nconst envSuffix = process.env.ENVIRONMENT ? `-${process.env.ENVIRONMENT}` : \"\";\nconst typeSuffix = dataOnly ? \"-data\" : includeData ? \"-full\" : \"-schema\";\nconst tableSuffix = table ? `-${table}` : \"\";\n\n// Ensure backups directory exists\nconst backupsDir = resolve(\"./backups\");\nif (!existsSync(backupsDir)) {\n  await mkdir(backupsDir, { recursive: true });\n  console.log(`📁 Created backups directory: ${backupsDir}`);\n}\n\nconst outputPath = resolve(\n  backupsDir,\n  `dump${envSuffix}${typeSuffix}${tableSuffix}-${timestamp}.sql`,\n);\n\npgDumpArgs.push(`--file=${outputPath}`);\n\n// Check if pg_dump is available\ntry {\n  await $`which pg_dump`.quiet();\n} catch {\n  console.error(\n    \"❌ pg_dump not found. Please install PostgreSQL client tools.\",\n  );\n  process.exit(1);\n}\n\nconsole.log(\"📤 Exporting database...\");\nconsole.log(`📁 Output: ${outputPath}`);\n\ntry {\n  // Execute pg_dump\n  await $`pg_dump ${pgDumpArgs}`;\n\n  // Set file permissions to owner-only readable (600)\n  await chmod(outputPath, 0o600);\n\n  console.log(`✅ Export completed successfully!`);\n} catch (error) {\n  console.error(\"❌ Export failed:\");\n  console.error(error);\n  process.exit(1);\n}\n"
  },
  {
    "path": "db/scripts/generate-auth-schema.ts",
    "content": "import { getAuthTables } from \"better-auth/db\";\nimport type { BetterAuthOptions } from \"better-auth/types\";\nimport { createAuth } from \"../../apps/api/lib/auth\";\nimport { env } from \"../../apps/api/lib/env\";\n\n/**\n * Generates the complete database structure from Better Auth configuration\n * Outputs the schema as formatted JSON showing all tables, fields, and relationships\n */\nasync function generateAuthSchema() {\n  // Mock database instance - Better Auth only needs this for type checking, not actual queries\n  const mockDb = {} as Record<string, unknown>;\n\n  // Create the auth instance to get the configuration\n  const auth = createAuth(mockDb, {\n    APP_NAME: env.APP_NAME || \"React Starter Kit\",\n    APP_ORIGIN: env.APP_ORIGIN || \"http://localhost:3000\",\n    BETTER_AUTH_SECRET: env.BETTER_AUTH_SECRET || \"mock-secret\",\n    GOOGLE_CLIENT_ID: env.GOOGLE_CLIENT_ID || \"mock-client-id\",\n    GOOGLE_CLIENT_SECRET: env.GOOGLE_CLIENT_SECRET || \"mock-client-secret\",\n  });\n\n  // WARNING: Type assertion needed as Better Auth doesn't export the auth instance type\n  const authOptions = (auth as { options: BetterAuthOptions }).options;\n\n  // Get the complete database schema\n  const tables = getAuthTables(authOptions);\n\n  // Format the output for better readability\n  const schemaOutput = {\n    metadata: {\n      description: \"Better Auth database schema\",\n      generatedAt: new Date().toISOString(),\n      tableCount: Object.keys(tables).length,\n    },\n    tables: {},\n  };\n\n  // Process each table\n  for (const [tableKey, table] of Object.entries(tables)) {\n    const processedFields: Record<string, Record<string, unknown>> = {};\n\n    // Process each field in the table\n    for (const [fieldKey, field] of Object.entries(table.fields)) {\n      processedFields[fieldKey] = {\n        type: field.type,\n        required: field.required || false,\n        unique: field.unique || false,\n      };\n\n      // Add references if they exist\n      if (field.references) {\n        processedFields[fieldKey].references = {\n          model: field.references.model,\n          field: field.references.field,\n        };\n      }\n    }\n\n    (schemaOutput.tables as Record<string, unknown>)[tableKey] = {\n      modelName: table.modelName,\n      fields: processedFields,\n    };\n  }\n\n  return schemaOutput;\n}\n\n// Main execution\nasync function main() {\n  try {\n    const schema = await generateAuthSchema();\n    console.log(JSON.stringify(schema, null, 2));\n  } catch (error) {\n    console.error(\"Error generating auth schema:\", error);\n    process.exit(1);\n  }\n}\n\nif (require.main === module) {\n  main();\n}\n\nexport { generateAuthSchema };\n"
  },
  {
    "path": "db/scripts/seed.ts",
    "content": "#!/usr/bin/env bun\n// Usage: bun scripts/seed.ts [--env ENVIRONMENT=staging|prod]\n\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\nimport * as schema from \"../schema\";\nimport { seedUsers } from \"../seeds/users\";\n\n// Import drizzle config to trigger environment loading\nimport \"../drizzle.config\";\n\nconst client = postgres(process.env.DATABASE_URL!, { max: 1 });\nconst db = drizzle(client, { schema, casing: \"snake_case\" });\n\nconsole.log(\"🌱 Starting database seeding...\");\n\ntry {\n  await seedUsers(db);\n  console.log(\"✅ Database seeding completed successfully!\");\n} catch (error) {\n  console.error(\"❌ Database seeding failed:\");\n  console.error(error);\n  process.exitCode = 1;\n} finally {\n  await client.end();\n}\n"
  },
  {
    "path": "db/seeds/users.ts",
    "content": "import { PostgresJsDatabase } from \"drizzle-orm/postgres-js\";\nimport * as schema from \"../schema\";\nimport { type NewUser, user } from \"../schema\";\n\n/**\n * Seeds the database with test user accounts.\n */\nexport async function seedUsers(db: PostgresJsDatabase<typeof schema>) {\n  console.log(\"Seeding users...\");\n\n  // Test user data with realistic names and email addresses\n  const users: NewUser[] = [\n    { name: \"Alice Johnson\", email: \"alice@example.com\", emailVerified: true },\n    { name: \"Bob Smith\", email: \"bob@example.com\", emailVerified: true },\n    {\n      name: \"Charlie Brown\",\n      email: \"charlie@example.com\",\n      emailVerified: false,\n    },\n    { name: \"Diana Prince\", email: \"diana@example.com\", emailVerified: true },\n    { name: \"Eve Davis\", email: \"eve@example.com\", emailVerified: true },\n    { name: \"Frank Miller\", email: \"frank@example.com\", emailVerified: false },\n    { name: \"Grace Lee\", email: \"grace@example.com\", emailVerified: true },\n    { name: \"Henry Wilson\", email: \"henry@example.com\", emailVerified: true },\n    { name: \"Ivy Chen\", email: \"ivy@example.com\", emailVerified: false },\n    { name: \"Jack Thompson\", email: \"jack@example.com\", emailVerified: true },\n  ];\n\n  for (const u of users) {\n    await db.insert(user).values(u).onConflictDoNothing();\n  }\n\n  console.log(`✅ Seeded ${users.length} test users`);\n}\n"
  },
  {
    "path": "db/tsconfig.json",
    "content": "{\n  \"extends\": \"../packages/typescript-config/node.jsonc\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"dist/.tsbuildinfo\",\n    \"composite\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"types\": [\"bun\", \"node\"]\n  },\n  \"include\": [\"schema/**/*.ts\", \"*.ts\", \"*.json\"],\n  \"exclude\": [\"**/dist/**/*\", \"**/node_modules/**/*\", \"scripts/**/*\"]\n}\n"
  },
  {
    "path": "docs/.vitepress/config.ts",
    "content": "import { defineConfig } from \"vitepress\";\nimport llmstxt from \"vitepress-plugin-llms\";\n\n/**\n * VitePress configuration.\n * @see https://vitepress.dev/reference/site-config\n */\nexport default defineConfig({\n  title: \"React Starter Kit\",\n  description: \"Production-ready monorepo for building fast web apps\",\n\n  markdown: {\n    config(md) {\n      const fence = md.renderer.rules.fence!;\n      md.renderer.rules.fence = (tokens, idx, options, env, self) => {\n        const token = tokens[idx];\n        if (token.info === \"mermaid\") {\n          const code = md.utils.escapeHtml(token.content.trim());\n          return `<Mermaid code=\"${code}\" />`;\n        }\n        return fence(tokens, idx, options, env, self);\n      };\n    },\n  },\n\n  lastUpdated: true,\n  cleanUrls: true,\n  metaChunk: true,\n  ignoreDeadLinks: [/%5B.*_URL%5D/],\n\n  sitemap: {\n    hostname: \"https://reactstarter.com\",\n    transformItems: (items) => {\n      items.push({ url: \"llms.txt\" }, { url: \"llms-full.txt\" });\n      return items;\n    },\n  },\n\n  head: [\n    [\"meta\", { name: \"theme-color\", content: \"#6366f1\" }],\n    [\"meta\", { property: \"og:type\", content: \"website\" }],\n    [\"meta\", { property: \"og:site_name\", content: \"React Starter Kit\" }],\n    [\n      \"link\",\n      {\n        rel: \"alternate\",\n        type: \"text/plain\",\n        href: \"/llms.txt\",\n        title: \"LLM context\",\n      },\n    ],\n    [\n      \"link\",\n      {\n        rel: \"alternate\",\n        type: \"text/plain\",\n        href: \"/llms-full.txt\",\n        title: \"LLM context (full)\",\n      },\n    ],\n  ],\n\n  themeConfig: {\n    // https://vitepress.dev/reference/default-theme-config\n    nav: [\n      { text: \"Home\", link: \"/\" },\n      { text: \"Docs\", link: \"/getting-started/\" },\n    ],\n\n    search: {\n      provider: \"local\",\n    },\n\n    editLink: {\n      pattern:\n        \"https://github.com/kriasoft/react-starter-kit/edit/main/docs/:path\",\n      text: \"Edit this page on GitHub\",\n    },\n\n    footer: {\n      message:\n        'LLM context: <a href=\"/llms.txt\">llms.txt</a> · <a href=\"/llms-full.txt\">llms-full.txt</a><br>Released under the MIT License.',\n      copyright: \"Copyright © 2014-present Kriasoft\",\n    },\n\n    sidebar: [\n      {\n        text: \"Getting Started\",\n        items: [\n          { text: \"Introduction\", link: \"/getting-started/\" },\n          { text: \"Quick Start\", link: \"/getting-started/quick-start\" },\n          {\n            text: \"Project Structure\",\n            link: \"/getting-started/project-structure\",\n          },\n          {\n            text: \"Environment Variables\",\n            link: \"/getting-started/environment-variables\",\n          },\n        ],\n      },\n      {\n        text: \"Architecture\",\n        items: [\n          { text: \"Overview\", link: \"/architecture/\" },\n          { text: \"Edge\", link: \"/architecture/edge\" },\n        ],\n      },\n      {\n        text: \"Frontend\",\n        collapsed: true,\n        items: [\n          { text: \"Routing\", link: \"/frontend/routing\" },\n          { text: \"State & Data Fetching\", link: \"/frontend/state\" },\n          { text: \"UI\", link: \"/frontend/ui\" },\n          { text: \"Forms & Validation\", link: \"/frontend/forms\" },\n        ],\n      },\n      {\n        text: \"API\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/api/\" },\n          { text: \"Procedures\", link: \"/api/procedures\" },\n          { text: \"Validation & Errors\", link: \"/api/validation-errors\" },\n          { text: \"Context & Middleware\", link: \"/api/context\" },\n        ],\n      },\n      {\n        text: \"Authentication\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/auth/\" },\n          { text: \"Email & OTP\", link: \"/auth/email-otp\" },\n          { text: \"Social Providers\", link: \"/auth/social-providers\" },\n          { text: \"Passkeys\", link: \"/auth/passkeys\" },\n          { text: \"Organizations & Roles\", link: \"/auth/organizations\" },\n          { text: \"Sessions & Protected Routes\", link: \"/auth/sessions\" },\n        ],\n      },\n      {\n        text: \"Database\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/database/\" },\n          { text: \"Schema\", link: \"/database/schema\" },\n          { text: \"Migrations\", link: \"/database/migrations\" },\n          { text: \"Seeding\", link: \"/database/seeding\" },\n          { text: \"Query Patterns\", link: \"/database/queries\" },\n        ],\n      },\n      {\n        text: \"Billing\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/billing/\" },\n          { text: \"Plans & Pricing\", link: \"/billing/plans\" },\n          { text: \"Checkout Flow\", link: \"/billing/checkout\" },\n          { text: \"Webhooks\", link: \"/billing/webhooks\" },\n        ],\n      },\n      { text: \"Email\", link: \"/email\" },\n      { text: \"Testing\", link: \"/testing\" },\n      {\n        text: \"Deployment\",\n        collapsed: true,\n        items: [\n          { text: \"Overview\", link: \"/deployment/\" },\n          { text: \"Cloudflare Workers\", link: \"/deployment/cloudflare\" },\n          {\n            text: \"Production Database\",\n            link: \"/deployment/production-database\",\n          },\n          { text: \"CI/CD\", link: \"/deployment/ci-cd\" },\n          { text: \"Monitoring\", link: \"/deployment/monitoring\" },\n        ],\n      },\n      {\n        text: \"Recipes\",\n        collapsed: true,\n        items: [\n          { text: \"Add a Page\", link: \"/recipes/new-page\" },\n          { text: \"Add a tRPC Procedure\", link: \"/recipes/new-procedure\" },\n          { text: \"Add a Database Table\", link: \"/recipes/new-table\" },\n          { text: \"Add Teams\", link: \"/recipes/teams\" },\n          { text: \"WebSockets\", link: \"/recipes/websockets\" },\n          { text: \"File Uploads\", link: \"/recipes/file-uploads\" },\n        ],\n      },\n      {\n        text: \"Security\",\n        collapsed: true,\n        items: [\n          { text: \"Security Checklist\", link: \"/security/checklist\" },\n          { text: \"Incident Playbook\", link: \"/security/incident-playbook\" },\n          {\n            text: \"Security Policy Template\",\n            link: \"/security/policy-template\",\n          },\n        ],\n      },\n    ],\n\n    socialLinks: [\n      {\n        icon: {\n          svg: '<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z\" fill=\"currentColor\"/></svg>',\n        },\n        link: \"https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant\",\n        ariaLabel: \"Ask GPT\",\n      },\n      {\n        icon: \"discord\",\n        link: \"https://discord.gg/2nKEnKq\",\n      },\n      {\n        icon: \"github\",\n        link: \"https://github.com/kriasoft/react-starter-kit\",\n      },\n    ],\n  },\n\n  vite: {\n    plugins: [llmstxt()],\n  },\n});\n"
  },
  {
    "path": "docs/.vitepress/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://reactstarter.com/sitemap.xml\nSitemap: https://reactstarter.com/llms.txt\n"
  },
  {
    "path": "docs/.vitepress/theme/components/GitHubStats.vue",
    "content": "<template>\n  <div class=\"github-stats\">\n    <a\n      href=\"https://github.com/kriasoft/react-starter-kit/stargazers\"\n      class=\"stat-item\"\n    >\n      <svg\n        class=\"stat-icon\"\n        aria-hidden=\"true\"\n        height=\"16\"\n        viewBox=\"0 0 16 16\"\n        version=\"1.1\"\n        width=\"16\"\n      >\n        <path\n          d=\"M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.192L.644 6.57a.75.75 0 0 1 .416-1.28l4.21-.611L7.14.67A.75.75 0 0 1 8 .25Z\"\n        ></path>\n      </svg>\n      <span class=\"stat-count\">23k</span>\n    </a>\n    <a\n      href=\"https://github.com/kriasoft/react-starter-kit/forks\"\n      class=\"stat-item\"\n    >\n      <svg\n        class=\"stat-icon\"\n        aria-hidden=\"true\"\n        height=\"16\"\n        viewBox=\"0 0 16 16\"\n        version=\"1.1\"\n        width=\"16\"\n      >\n        <path\n          d=\"M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.5 0a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm-3.25 8a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z\"\n        ></path>\n      </svg>\n      <span class=\"stat-count\">4.2k</span>\n    </a>\n  </div>\n</template>\n\n<style scoped>\n.github-stats {\n  display: flex;\n  align-items: center;\n  padding-left: 8px;\n}\n\n.stat-item {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  color: var(--vp-c-text-2);\n  font-size: 14px;\n  padding-left: 8px;\n  text-decoration: none;\n  transition: color 0.2s;\n}\n\n.stat-item:hover {\n  color: var(--vp-c-text-1);\n}\n\n.stat-icon {\n  fill: currentColor;\n  flex-shrink: 0;\n}\n\n.stat-count {\n  font-weight: 500;\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/theme/components/Mermaid.vue",
    "content": "<script setup lang=\"ts\">\nimport { ref, computed, onMounted, watch } from \"vue\";\nimport { useData } from \"vitepress\";\n\nconst props = defineProps<{ code: string }>();\nconst { isDark } = useData();\nconst container = ref<HTMLElement | null>(null);\nconst svg = ref(\"\");\n\nconst theme = computed(() => (isDark.value ? \"dark\" : \"default\"));\n\nconst liveUrl = computed(() => {\n  const state = JSON.stringify({\n    code: props.code,\n    mermaid: { theme: theme.value },\n  });\n  const bytes = new TextEncoder().encode(state);\n  const binary = Array.from(bytes, (b) => String.fromCodePoint(b)).join(\"\");\n  return `https://mermaid.live/view#base64:${btoa(binary)}`;\n});\n\nasync function render() {\n  if (!container.value || !props.code) return;\n\n  const { default: mermaid } = await import(\"mermaid\");\n  mermaid.initialize({\n    startOnLoad: false,\n    theme: theme.value,\n    securityLevel: \"loose\",\n  });\n\n  const id = `mermaid-${Math.random().toString(36).slice(2)}`;\n  const { svg: rendered } = await mermaid.render(id, props.code);\n  svg.value = rendered;\n}\n\nonMounted(render);\nwatch([() => props.code, theme], render);\n</script>\n\n<template>\n  <div class=\"mermaid-wrapper\">\n    <div ref=\"container\" class=\"mermaid-container\" v-html=\"svg\" />\n    <a\n      :href=\"liveUrl\"\n      target=\"_blank\"\n      rel=\"noopener\"\n      class=\"fullscreen-link\"\n      title=\"Open in fullscreen\"\n    >\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"16\"\n        height=\"16\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        stroke-width=\"2\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n      >\n        <path d=\"M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7\" />\n      </svg>\n    </a>\n  </div>\n</template>\n\n<style scoped>\n.mermaid-wrapper {\n  position: relative;\n  margin: 1.5rem 0;\n}\n\n.mermaid-container {\n  display: flex;\n  justify-content: center;\n  overflow-x: auto;\n  padding: 1rem;\n  border-radius: 8px;\n  background-color: var(--vp-c-bg-soft);\n}\n\n.mermaid-container :deep(svg) {\n  max-width: 100%;\n  height: auto;\n}\n\n.fullscreen-link {\n  position: absolute;\n  top: 0.5rem;\n  right: 0.5rem;\n  padding: 0.5rem;\n  border-radius: 4px;\n  color: var(--vp-c-text-2);\n  background: var(--vp-c-bg);\n  opacity: 0;\n  transition:\n    opacity 0.2s,\n    color 0.2s;\n}\n\n.mermaid-wrapper:hover .fullscreen-link {\n  opacity: 1;\n}\n\n.fullscreen-link:hover {\n  color: var(--vp-c-brand-1);\n}\n</style>\n"
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "content": "/**\n * Custom theme for VitePress documentation.\n * @see https://vitepress.dev/guide/custom-theme\n */\n\nimport type { Theme } from \"vitepress\";\nimport DefaultTheme from \"vitepress/theme\";\nimport { h } from \"vue\";\nimport GitHubStats from \"./components/GitHubStats.vue\";\nimport Mermaid from \"./components/Mermaid.vue\";\nimport \"./style.css\";\n\nexport default {\n  extends: DefaultTheme,\n  Layout: () => {\n    return h(DefaultTheme.Layout, null, {\n      // https://vitepress.dev/guide/extending-default-theme#layout-slots\n      \"nav-bar-content-after\": () => h(GitHubStats),\n    });\n  },\n  enhanceApp({ app }) {\n    app.component(\"Mermaid\", Mermaid);\n  },\n} satisfies Theme;\n"
  },
  {
    "path": "docs/.vitepress/theme/style.css",
    "content": "/**\n * Customize default theme styling by overriding CSS variables:\n * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css\n */\n\n/**\n * Colors\n *\n * Each colors have exact same color scale system with 3 levels of solid\n * colors with different brightness, and 1 soft color.\n *\n * - `XXX-1`: The most solid color used mainly for colored text. It must\n *   satisfy the contrast ratio against when used on top of `XXX-soft`.\n *\n * - `XXX-2`: The color used mainly for hover state of the button.\n *\n * - `XXX-3`: The color for solid background, such as bg color of the button.\n *   It must satisfy the contrast ratio with pure white (#ffffff) text on\n *   top of it.\n *\n * - `XXX-soft`: The color used for subtle background such as custom container\n *   or badges. It must satisfy the contrast ratio when putting `XXX-1` colors\n *   on top of it.\n *\n *   The soft color must be semi transparent alpha channel. This is crucial\n *   because it allows adding multiple \"soft\" colors on top of each other\n *   to create a accent, such as when having inline code block inside\n *   custom containers.\n *\n * - `default`: The color used purely for subtle indication without any\n *   special meanings attached to it such as bg color for menu hover state.\n *\n * - `brand`: Used for primary brand colors, such as link text, button with\n *   brand theme, etc.\n *\n * - `tip`: Used to indicate useful information. The default theme uses the\n *   brand color for this by default.\n *\n * - `warning`: Used to indicate warning to the users. Used in custom\n *   container, badges, etc.\n *\n * - `danger`: Used to show error, or dangerous message to the users. Used\n *   in custom container, badges, etc.\n * -------------------------------------------------------------------------- */\n\n:root {\n  --vp-c-default-1: var(--vp-c-gray-1);\n  --vp-c-default-2: var(--vp-c-gray-2);\n  --vp-c-default-3: var(--vp-c-gray-3);\n  --vp-c-default-soft: var(--vp-c-gray-soft);\n\n  --vp-c-brand-1: var(--vp-c-indigo-1);\n  --vp-c-brand-2: var(--vp-c-indigo-2);\n  --vp-c-brand-3: var(--vp-c-indigo-3);\n  --vp-c-brand-soft: var(--vp-c-indigo-soft);\n\n  --vp-c-tip-1: var(--vp-c-brand-1);\n  --vp-c-tip-2: var(--vp-c-brand-2);\n  --vp-c-tip-3: var(--vp-c-brand-3);\n  --vp-c-tip-soft: var(--vp-c-brand-soft);\n\n  --vp-c-warning-1: var(--vp-c-yellow-1);\n  --vp-c-warning-2: var(--vp-c-yellow-2);\n  --vp-c-warning-3: var(--vp-c-yellow-3);\n  --vp-c-warning-soft: var(--vp-c-yellow-soft);\n\n  --vp-c-danger-1: var(--vp-c-red-1);\n  --vp-c-danger-2: var(--vp-c-red-2);\n  --vp-c-danger-3: var(--vp-c-red-3);\n  --vp-c-danger-soft: var(--vp-c-red-soft);\n}\n\n/**\n * Component: Button\n * -------------------------------------------------------------------------- */\n\n:root {\n  --vp-button-brand-border: transparent;\n  --vp-button-brand-text: var(--vp-c-white);\n  --vp-button-brand-bg: var(--vp-c-brand-3);\n  --vp-button-brand-hover-border: transparent;\n  --vp-button-brand-hover-text: var(--vp-c-white);\n  --vp-button-brand-hover-bg: var(--vp-c-brand-2);\n  --vp-button-brand-active-border: transparent;\n  --vp-button-brand-active-text: var(--vp-c-white);\n  --vp-button-brand-active-bg: var(--vp-c-brand-1);\n}\n\n/**\n * Component: Home\n * -------------------------------------------------------------------------- */\n\n:root {\n  --vp-home-hero-name-color: transparent;\n  --vp-home-hero-name-background: -webkit-linear-gradient(\n    120deg,\n    #bd34fe 30%,\n    #41d1ff\n  );\n\n  --vp-home-hero-image-background-image: linear-gradient(\n    -45deg,\n    #bd34fe 50%,\n    #47caff 50%\n  );\n  --vp-home-hero-image-filter: blur(44px);\n}\n\n@media (min-width: 640px) {\n  :root {\n    --vp-home-hero-image-filter: blur(56px);\n  }\n}\n\n@media (min-width: 960px) {\n  :root {\n    --vp-home-hero-image-filter: blur(68px);\n  }\n}\n\n/**\n * Component: Custom Block\n * -------------------------------------------------------------------------- */\n\n:root {\n  --vp-custom-block-tip-border: transparent;\n  --vp-custom-block-tip-text: var(--vp-c-text-1);\n  --vp-custom-block-tip-bg: var(--vp-c-brand-soft);\n  --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);\n}\n\n/**\n * Component: Algolia\n * -------------------------------------------------------------------------- */\n\n.DocSearch {\n  --docsearch-primary-color: var(--vp-c-brand-1) !important;\n}\n"
  },
  {
    "path": "docs/adr/000-template.md",
    "content": "# ADR-NNN Title\n\n**Status:** Proposed | Accepted | Deprecated | Superseded  \n**Date:** YYYY-MM-DD  \n**Tags:** tag1, tag2\n\n## Problem\n\n- One or two sentences on the decision trigger or constraint.\n\n## Decision\n\n- The chosen approach in a short paragraph.\n\n## Alternatives (brief)\n\n- Option A – why not.\n- Option B – why not.\n\n## Impact\n\n- Positive:\n- Negative/Risks:\n\n## Links\n\n- Code/Docs:\n- Related ADRs:\n"
  },
  {
    "path": "docs/adr/001-auth-hint-cookie.md",
    "content": "# ADR-001 Auth Hint Cookie For Edge Routing\n\n**Status:** Accepted\n**Date:** 2025-12-28\n**Tags:** auth, routing, edge\n\n## Problem\n\nThe web edge needs a fast signal to route `/` without owning auth logic.\n\n## Decision\n\nUse a dedicated auth-hint cookie set on login and cleared on logout or invalid session. The web worker checks only cookie presence to route, while the app remains the authority. No API calls or session validation in `web`.\n\nThis cookie is NOT a security boundary. It is a routing hint only. False positives are acceptable and result in one extra redirect to `/login`.\n\n## Implementation Notes\n\n- Cookie name: `__Host-auth` in HTTPS; `auth` in HTTP dev (browsers reject `__Host-` without Secure).\n- Cookie lifecycle: set on new session; clear on sign-out; clear on session-check failure.\n- Web routing: check for either cookie name; never read session cookies.\n\n## Alternatives Considered\n\n1. **Validate session in web via API** – Couples edge to auth, adds latency/failure modes.\n2. **Read Better Auth session cookie directly** – Brittle to auth library changes and cookie formats.\n\n## Consequences\n\n- **Positive:** Faster edge routing, clear separation of concerns, auth-lib agnostic.\n- **Negative:** False positives cause one extra redirect; requires maintaining set/clear hooks.\n\n## Links\n\n- https://github.com/kriasoft/react-starter-kit/issues/2101\n"
  },
  {
    "path": "docs/api/context.md",
    "content": "# Context & Middleware\n\nEvery tRPC procedure receives a context object (`ctx`) with request-scoped resources. The middleware chain builds this context before any procedure runs.\n\n## TRPCContext\n\nDefined in `apps/api/lib/context.ts`, the context provides:\n\n| Field         | Type                               | Description                                             |\n| ------------- | ---------------------------------- | ------------------------------------------------------- |\n| `req`         | `Request`                          | The incoming HTTP request                               |\n| `info`        | `CreateHTTPContextOptions[\"info\"]` | tRPC request metadata (headers, connection info)        |\n| `db`          | `PostgresJsDatabase`               | Drizzle ORM instance via Hyperdrive (cached connection) |\n| `dbDirect`    | `PostgresJsDatabase`               | Drizzle ORM instance via Hyperdrive (direct, no cache)  |\n| `session`     | `AuthSession \\| null`              | Authenticated session from Better Auth                  |\n| `user`        | `AuthUser \\| null`                 | Authenticated user data                                 |\n| `cache`       | `Map<string \\| symbol, unknown>`   | Request-scoped cache (for DataLoaders, computed values) |\n| `res?`        | `Response`                         | Optional HTTP response from Hono context                |\n| `resHeaders?` | `Headers`                          | Response headers (for setting cookies, etc.)            |\n| `env`         | `Env`                              | Environment variables and secrets                       |\n\n### Two Database Connections\n\nThe context provides two database connections with different caching behaviors:\n\n- **`ctx.db`** – routed through Cloudflare Hyperdrive's connection pool with query caching. Use for read-heavy queries.\n- **`ctx.dbDirect`** – bypasses the cache. Use for writes, transactions, and reads that must see the latest data.\n\n```ts\n// Read with caching\nconst users = await ctx.db.select().from(user);\n\n// Write via direct connection\nawait ctx.dbDirect.insert(post).values({ title: \"Hello\" });\n```\n\n## How Context is Constructed\n\nContext is created per-request in the tRPC fetch adapter (`apps/api/lib/app.ts`):\n\n```ts\napp.use(\"/api/trpc/*\", (c) => {\n  return fetchRequestHandler({\n    req: c.req.raw,\n    router: appRouter,\n    endpoint: \"/api/trpc\",\n    async createContext({ req, resHeaders, info }) {\n      const db = c.get(\"db\");\n      const dbDirect = c.get(\"dbDirect\");\n      const auth = c.get(\"auth\");\n\n      if (!db) throw new Error(\"Database not available in context\");\n      if (!dbDirect)\n        throw new Error(\"Direct database not available in context\");\n      if (!auth)\n        throw new Error(\"Authentication service not available in context\");\n\n      const sessionData = await auth.api.getSession({\n        headers: req.headers,\n      });\n\n      return {\n        req,\n        res: c.res,\n        resHeaders,\n        info,\n        env: c.env,\n        db,\n        dbDirect,\n        session: sessionData?.session ?? null,\n        user: sessionData?.user ?? null,\n        cache: new Map(),\n      };\n    },\n    batching: { enabled: true },\n  });\n});\n```\n\nThe `db`, `dbDirect`, and `auth` values come from the Hono middleware layer (set in `worker.ts`). The tRPC context adds session resolution and a fresh `cache` Map.\n\n## Middleware Chain\n\nThe Worker entrypoint (`worker.ts`) applies middleware in order:\n\n```txt\nRequest\n  │\n  ├── errorHandler          ← catches all unhandled errors\n  ├── notFoundHandler       ← returns 404 JSON for unmatched routes\n  │\n  ├── secureHeaders()       ← security headers (CSP, X-Frame-Options, etc.)\n  ├── requestId()           ← generates X-Request-Id (uses CF-Ray if available)\n  ├── logger()              ← logs request method, path, status, duration\n  │\n  ├── context init          ← creates db, dbDirect, auth; sets on Hono context\n  │\n  └── app.ts routes\n        ├── /api/auth/*     ← Better Auth (session resolved internally)\n        └── /api/trpc/*     ← tRPC (session resolved in createContext)\n```\n\n::: info\nThe `protectedProcedure` middleware (defined in `lib/trpc.ts`) adds another layer within tRPC. It checks that `session` and `user` are non-null and narrows their types – procedures using `protectedProcedure` never need null checks. See [Procedures](./procedures#protectedprocedure).\n:::\n\n::: tip\nIn production (`worker.ts`), the request ID generator uses the Cloudflare Ray ID when available. In local development (`dev.ts`), it falls back to the default UUID generator since `cf-ray` headers aren't present.\n:::\n\n## Request ID\n\nThe request ID middleware uses the Cloudflare Ray ID when available, falling back to `crypto.randomUUID()` in local development:\n\n```ts\nexport function requestIdGenerator(c: Context): string {\n  return c.req.header(\"cf-ray\") ?? crypto.randomUUID();\n}\n```\n\nThe ID is available via the `X-Request-Id` response header for tracing requests across logs.\n\n## DataLoaders\n\nDataLoaders prevent N+1 queries by batching multiple `.load(id)` calls into a single SQL `WHERE IN (...)` query. They're defined in `apps/api/lib/loaders.ts` and cached per-request via `ctx.cache`.\n\n```ts\nimport { userById } from \"../lib/loaders.js\";\n\nmembers: protectedProcedure\n  .input(z.object({ organizationId: z.string() }))\n  .query(async ({ ctx, input }) => {\n    const members = await ctx.db.query.member.findMany({\n      where: (m, { eq }) => eq(m.organizationId, input.organizationId),\n    });\n\n    // Batches all user lookups into one query\n    const users = await Promise.all(\n      members.map((m) => userById(ctx).load(m.userId)),\n    );\n\n    return members.map((m, i) => ({ ...m, user: users[i] }));\n  }),\n```\n\nLoaders are created with a `defineLoader` helper that handles per-request caching via `ctx.cache`:\n\n```ts\nfunction defineLoader<K, V>(\n  key: symbol,\n  batchFn: (ctx: TRPCContext, keys: readonly K[]) => Promise<(V | null)[]>,\n): (ctx: TRPCContext) => DataLoader<K, V | null>;\n```\n\nEach call returns a factory `(ctx) => DataLoader`. The first invocation per request creates the instance; subsequent calls return the cached one. Because `ctx.cache` is a `Map` created per-request, loaders are automatically scoped to the request lifecycle – no stale data across requests.\n\n### Adding a DataLoader\n\nAdd a `defineLoader` call in `apps/api/lib/loaders.ts`:\n\n```ts\nexport const postById = defineLoader(\n  Symbol(\"postById\"),\n  async (ctx, ids: readonly string[]) => {\n    const posts = await ctx.db\n      .select()\n      .from(post)\n      .where(inArray(post.id, [...ids]));\n    return mapByKey(posts, \"id\", ids);\n  },\n);\n```\n\nThen call `.load(key)` or `.loadMany(keys)` in your procedures.\n"
  },
  {
    "path": "docs/api/index.md",
    "content": "---\noutline: [2, 3]\n---\n\n# API Overview\n\nThe API server (`apps/api/`) runs as a Cloudflare Worker and handles all backend logic: authentication, data access, and billing webhooks. It combines two frameworks:\n\n- **[Hono](https://hono.dev/)** – lightweight HTTP router for auth endpoints, webhooks, and health checks\n- **[tRPC](https://trpc.io/)** – type-safe RPC layer for all client-facing queries and mutations\n\nHono handles the HTTP surface. tRPC handles the typed contract between frontend and backend. They share the same Worker and middleware stack.\n\n## How the Worker is Wired\n\nThe API has two entrypoints – one for production (Cloudflare Workers) and one for local development (Bun):\n\n| File        | Runtime            | Description                                    |\n| ----------- | ------------------ | ---------------------------------------------- |\n| `worker.ts` | Cloudflare Workers | Production entrypoint                          |\n| `dev.ts`    | Bun                | Local dev server via `wrangler` platform proxy |\n\nBoth follow the same structure:\n\n```\nworker.ts / dev.ts\n  ├── errorHandler, notFoundHandler\n  ├── secureHeaders()\n  ├── requestId()\n  ├── logger()\n  ├── context init (db, dbDirect, auth)\n  └── mount app.ts\n        ├── GET  /api          → API info (JSON)\n        ├── GET  /health       → health check\n        ├── *    /api/auth/*   → Better Auth handler\n        └── *    /api/trpc/*   → tRPC fetch adapter\n```\n\nThe top-level worker (`worker.ts`) sets up global middleware and initializes shared resources, then mounts the core Hono app (`lib/app.ts`) which defines the actual routes.\n\n```ts\n// apps/api/worker.ts (simplified)\nconst worker = new Hono();\n\nworker.onError(errorHandler);\nworker.notFound(notFoundHandler);\nworker.use(secureHeaders());\nworker.use(requestId({ generator: requestIdGenerator }));\nworker.use(logger());\n\n// Initialize shared context\nworker.use(async (c, next) => {\n  const db = createDb(c.env.HYPERDRIVE_CACHED);\n  const dbDirect = createDb(c.env.HYPERDRIVE_DIRECT);\n\n  c.set(\"db\", db);\n  c.set(\"dbDirect\", dbDirect);\n  c.set(\"auth\", createAuth(db, c.env));\n  await next();\n});\n\n// Mount the core app\nworker.route(\"/\", app);\n```\n\n## Endpoints\n\n| Path          | Method    | Handler     | Description                                                                    |\n| ------------- | --------- | ----------- | ------------------------------------------------------------------------------ |\n| `/`           | GET       | Hono        | Redirects to `/api`                                                            |\n| `/api`        | GET       | Hono        | API metadata (name, version, endpoints)                                        |\n| `/health`     | GET       | Hono        | Health check – returns `{ status, timestamp }`                                 |\n| `/api/auth/*` | GET, POST | Better Auth | Authentication routes ([docs](https://www.better-auth.com/docs/api-reference)) |\n| `/api/trpc/*` | \\*        | tRPC        | Type-safe RPC – all queries and mutations                                      |\n\n## tRPC Router\n\nThe root router merges domain-specific sub-routers:\n\n```ts\n// apps/api/lib/app.ts\nconst appRouter = router({\n  billing: billingRouter,\n  user: userRouter,\n  organization: organizationRouter,\n});\n```\n\nEach sub-router lives in `routers/` and exports a single router instance. See [Procedures](./procedures) for details on adding your own.\n\n## Project Structure\n\n```bash\napps/api/\n├── worker.ts              # Cloudflare Workers entrypoint\n├── dev.ts                 # Local dev server (Bun)\n├── index.ts               # Public package exports\n├── lib/\n│   ├── ai.ts              # OpenAI provider factory\n│   ├── app.ts             # Hono app + tRPC router composition\n│   ├── auth.ts            # Better Auth configuration\n│   ├── context.ts         # TRPCContext and AppContext types\n│   ├── db.ts              # Drizzle ORM database factory\n│   ├── email.ts           # Resend email utilities\n│   ├── env.ts             # Environment variable schema (Zod)\n│   ├── loaders.ts         # DataLoader instances for N+1 prevention\n│   ├── middleware.ts       # Error handler, 404 handler, request ID\n│   ├── plans.ts           # Subscription plan limits\n│   ├── stripe.ts          # Stripe client factory\n│   └── trpc.ts            # tRPC init, procedures, error formatter\n├── routers/\n│   ├── billing.ts         # Subscription queries\n│   ├── billing.test.ts    # Billing router tests\n│   ├── organization.ts    # Organization CRUD\n│   └── user.ts            # User profile queries\n└── wrangler.jsonc         # Cloudflare Workers config\n```\n\n## Calling the API from the Frontend\n\nThe frontend app (`apps/app/`) uses `@trpc/client` with TanStack Query integration. The tRPC client is configured in `apps/app/lib/trpc.ts`:\n\n```ts\nimport { createTRPCOptionsProxy } from \"@trpc/tanstack-react-query\";\n\nexport const api = createTRPCOptionsProxy<AppRouter>({\n  client: trpcClient,\n  queryClient,\n});\n```\n\nUse `api` in components to call procedures with full type safety:\n\n```ts\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport { api } from \"~/lib/trpc\";\n\nfunction Profile() {\n  const { data } = useSuspenseQuery(api.user.me.queryOptions());\n  return <p>{data.name}</p>;\n}\n```\n\nSee the [tRPC + TanStack Query docs](https://trpc.io/docs/client/react/tanstack-react-query) for the full client API.\n"
  },
  {
    "path": "docs/api/procedures.md",
    "content": "# Procedures\n\ntRPC procedures are the primary way the frontend communicates with the API. Each procedure is either a **query** (read data) or a **mutation** (write data), with optional input validation via Zod.\n\n## Procedure Types\n\nThe project defines two base procedures in `apps/api/lib/trpc.ts`:\n\n### `publicProcedure`\n\nAccessible to all callers, including unauthenticated users. Context includes `db`, `env`, and `cache` but `session` and `user` may be `null`.\n\n```ts\nimport { publicProcedure } from \"../lib/trpc.js\";\n\nexport const healthRouter = router({\n  ping: publicProcedure.query(() => {\n    return { status: \"ok\" };\n  }),\n});\n```\n\n### `protectedProcedure`\n\nRequires an authenticated session. Throws `UNAUTHORIZED` if the user is not logged in. Context narrows `session` and `user` to non-null types – no runtime null checks needed.\n\n```ts\nimport { protectedProcedure } from \"../lib/trpc.js\";\n\nexport const userRouter = router({\n  me: protectedProcedure.query(async ({ ctx }) => {\n    return {\n      id: ctx.user.id, // ✓ guaranteed non-null\n      email: ctx.user.email,\n      name: ctx.user.name,\n    };\n  }),\n});\n```\n\n## Router Files\n\nEach domain gets its own router file in `apps/api/routers/`:\n\n```\nrouters/\n├── billing.ts         # billing.subscription\n├── organization.ts    # organization.list, .create, .update, .delete, ...\n└── user.ts            # user.me, .updateProfile, .list\n```\n\nRouters are merged into the root `appRouter` in `apps/api/lib/app.ts`:\n\n```ts\nconst appRouter = router({\n  billing: billingRouter,\n  user: userRouter,\n  organization: organizationRouter,\n});\n```\n\nThe client calls procedures using the namespace: `api.user.me`, `api.billing.subscription`, etc.\n\n## Input Validation\n\nDefine inputs with Zod schemas. tRPC validates them automatically and returns structured errors on failure (see [Validation & Errors](./validation-errors)).\n\n```ts\nimport { z } from \"zod\";\n\nexport const userRouter = router({\n  updateProfile: protectedProcedure\n    .input(\n      z.object({\n        name: z.string().min(1).optional(),\n        email: z.email({ error: \"Invalid email address\" }).optional(),\n      }),\n    )\n    .mutation(({ input, ctx }) => {\n      // `input` is fully typed: { name?: string; email?: string }\n      return { id: ctx.user.id, ...input };\n    }),\n});\n```\n\nFor queries with pagination:\n\n```ts\nlist: protectedProcedure\n  .input(\n    z.object({\n      limit: z.number().min(1).max(100).default(10),\n      cursor: z.string().optional(),\n    }),\n  )\n  .query(({ input }) => {\n    // input.limit defaults to 10 if not provided\n    return { users: [], nextCursor: null };\n  }),\n```\n\n## Adding a New Procedure\n\n**1. Create the router file** (or add to an existing one):\n\n```ts\n// apps/api/routers/post.ts\nimport { z } from \"zod\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const postRouter = router({\n  list: protectedProcedure\n    .input(z.object({ limit: z.number().max(50).default(20) }))\n    .query(async ({ ctx, input }) => {\n      return ctx.db.query.post.findMany({ limit: input.limit });\n    }),\n\n  create: protectedProcedure\n    .input(z.object({ title: z.string().min(1), body: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      // Insert into database\n    }),\n});\n```\n\n**2. Register the router** in `apps/api/lib/app.ts`:\n\n```ts\nimport { postRouter } from \"../routers/post.js\";\n\nconst appRouter = router({\n  billing: billingRouter,\n  user: userRouter,\n  organization: organizationRouter,\n  post: postRouter, // [!code ++]\n});\n```\n\n**3. Call from the frontend** – the types propagate automatically:\n\n```ts\nconst { data } = useSuspenseQuery(api.post.list.queryOptions({ limit: 10 }));\n```\n\n## Naming Conventions\n\n- **Router files**: singular noun matching the domain (`user.ts`, `billing.ts`, `organization.ts`)\n- **Router variables**: `{domain}Router` – `userRouter`, `billingRouter`\n- **Procedure names**: verb or short phrase – `me`, `list`, `create`, `updateProfile`\n- **Namespace key**: matches the domain – `user:`, `billing:`, `organization:`\n\n## Testing Procedures\n\nUse `createCallerFactory` to test procedures without HTTP:\n\n```ts\nimport { createCallerFactory } from \"../lib/trpc\";\nimport { billingRouter } from \"./billing\";\n\nconst createCaller = createCallerFactory(billingRouter);\n\nit(\"returns free plan defaults\", async () => {\n  const caller = createCaller(mockContext());\n  const result = await caller.subscription();\n  expect(result.plan).toBe(\"free\");\n});\n```\n\n<!-- See Testing docs for mock context patterns and more examples. -->\n"
  },
  {
    "path": "docs/api/validation-errors.md",
    "content": "# Validation & Errors\n\nInput validation and error handling follow one flow: Zod schemas validate procedure inputs, validation failures produce tRPC errors, and the error formatter attaches structured details for the client.\n\n## Input Validation\n\nEvery tRPC procedure can define a Zod schema via `.input()`. tRPC runs validation automatically before the procedure body executes.\n\n```ts\nupdateProfile: protectedProcedure\n  .input(\n    z.object({\n      name: z.string().min(1).optional(),\n      email: z.email({ error: \"Invalid email address\" }).optional(),\n    }),\n  )\n  .mutation(({ input }) => {\n    // Only runs if input passes validation\n  }),\n```\n\nWhen validation fails, tRPC returns a `BAD_REQUEST` error with the Zod error attached (see [Error Formatter](#error-formatter) below).\n\n## Error Formatter\n\nThe tRPC initialization in `apps/api/lib/trpc.ts` includes a custom error formatter that attaches Zod validation details to the response:\n\n```ts\nconst t = initTRPC.context<TRPCContext>().create({\n  errorFormatter({ shape, error }) {\n    return {\n      ...shape,\n      data: {\n        ...shape.data,\n        zodError:\n          error.cause instanceof ZodError ? flattenError(error.cause) : null,\n      },\n    };\n  },\n});\n```\n\nThis means every error response includes a `zodError` field – either a [flattened Zod error](https://zod.dev/api?id=flattenerror) object or `null`. Clients can use this for field-level error display.\n\nExample error response for a failed validation:\n\n```json\n{\n  \"error\": {\n    \"message\": \"...\",\n    \"code\": -32600,\n    \"data\": {\n      \"code\": \"BAD_REQUEST\",\n      \"zodError\": {\n        \"formErrors\": [],\n        \"fieldErrors\": {\n          \"email\": [\"Invalid email address\"]\n        }\n      }\n    }\n  }\n}\n```\n\n## Throwing Errors in Procedures\n\nFor business logic errors, throw `TRPCError` with an appropriate code:\n\n```ts\nimport { TRPCError } from \"@trpc/server\";\n\ncreate: protectedProcedure\n  .input(z.object({ name: z.string().min(1) }))\n  .mutation(async ({ ctx, input }) => {\n    const existing = await ctx.db.query.organization.findFirst({\n      where: (o, { eq }) => eq(o.name, input.name),\n    });\n\n    if (existing) {\n      throw new TRPCError({\n        code: \"CONFLICT\",\n        message: \"Organization name already taken\",\n      });\n    }\n\n    // ... create organization\n  }),\n```\n\nCommon tRPC error codes:\n\n| Code                    | HTTP Status | When to Use                                             |\n| ----------------------- | ----------- | ------------------------------------------------------- |\n| `BAD_REQUEST`           | 400         | Invalid input (automatic from Zod)                      |\n| `UNAUTHORIZED`          | 401         | Not authenticated (automatic from `protectedProcedure`) |\n| `FORBIDDEN`             | 403         | Authenticated but lacking permission                    |\n| `NOT_FOUND`             | 404         | Resource doesn't exist                                  |\n| `CONFLICT`              | 409         | Duplicate or conflicting state                          |\n| `INTERNAL_SERVER_ERROR` | 500         | Unexpected server error                                 |\n\nSee the full list in the [tRPC error codes reference](https://trpc.io/docs/server/error-handling#error-codes).\n\n## HTTP Error Handling\n\nHono middleware in `apps/api/lib/middleware.ts` catches errors outside the tRPC layer:\n\n```ts\nexport const errorHandler: ErrorHandler = (err, c) => {\n  if (err instanceof HTTPException) {\n    // Merge middleware headers (CORS, security) into the exception response\n    const res = err.getResponse();\n    const headers = new Headers(res.headers);\n    c.res.headers.forEach((v, k) => headers.set(k, v));\n    return new Response(res.body, {\n      status: res.status,\n      statusText: res.statusText,\n      headers,\n    });\n  }\n  console.error(`[${c.req.method}] ${c.req.path}:`, err);\n  return c.json({ error: \"Internal Server Error\" }, 500);\n};\n```\n\n- **`HTTPException`** (from Hono) – merges middleware headers (security, CORS) into the exception's response before returning it. Used by Better Auth and webhook handlers.\n- **Unexpected errors** – logged and returned as a generic 500.\n\nThe tRPC adapter also logs errors independently:\n\n```ts\nonError({ error, path }) {\n  console.error(\"tRPC error on path\", path, \":\", error);\n},\n```\n\n## Client-Side Error Handling\n\nThe frontend app provides three utilities in `apps/app/lib/errors.ts` for working with errors from both tRPC and Better Auth:\n\n### `getErrorStatus(error)`\n\nExtracts the HTTP status code from various error shapes:\n\n```ts\nimport { getErrorStatus } from \"~/lib/errors\";\n\ntry {\n  await trpcClient.organization.create.mutate({ name: \"\" });\n} catch (err) {\n  const status = getErrorStatus(err); // 400\n}\n```\n\n### `isUnauthenticatedError(error)`\n\nChecks if the error indicates a 401 / `UNAUTHORIZED` state. Useful for triggering redirects to login:\n\n```ts\nimport { isUnauthenticatedError } from \"~/lib/errors\";\n\nif (isUnauthenticatedError(error)) {\n  navigate({ to: \"/login\" });\n}\n```\n\n::: tip\n`isUnauthenticatedError` checks for HTTP 401 and tRPC `UNAUTHORIZED` code. It does **not** match 403 (Forbidden) – that means authenticated but lacking permission.\n:::\n\n### `getErrorMessage(error)`\n\nSafely extracts a human-readable message from any thrown value:\n\n```ts\nimport { getErrorMessage } from \"~/lib/errors\";\n\nconst message = getErrorMessage(error);\n// \"Organization name already taken\" or \"An unexpected error occurred\"\n```\n"
  },
  {
    "path": "docs/architecture/edge.md",
    "content": "# Edge\n\nImplementation details for the Cloudflare Workers deployment. Read the [Architecture Overview](./) first for the mental model.\n\n## Workers Configuration\n\nEach worker has its own `wrangler.jsonc` in its workspace directory:\n\n| Worker | Config                    | `nodejs_compat` |  Static assets  |     Service bindings     |\n| ------ | ------------------------- | :-------------: | :-------------: | :----------------------: |\n| web    | `apps/web/wrangler.jsonc` |       No        | Marketing pages | APP_SERVICE, API_SERVICE |\n| app    | `apps/app/wrangler.jsonc` |       No        |   SPA bundle    |            –             |\n| api    | `apps/api/wrangler.jsonc` |       Yes       |        –        |            –             |\n\nThe API worker enables `nodejs_compat` for packages that depend on Node.js built-ins (e.g. `postgres`, `crypto`). The web and app workers don't need it – they only serve static assets and proxy requests.\n\n## Service Bindings\n\nService bindings are **non-inheritable** in Wrangler – the top-level declaration only applies to production. Each environment must redeclare its bindings with the correct worker names.\n\n```jsonc\n// apps/web/wrangler.jsonc\n{\n  // Production (top-level)\n  \"services\": [\n    { \"binding\": \"APP_SERVICE\", \"service\": \"example-app\" },\n    { \"binding\": \"API_SERVICE\", \"service\": \"example-api\" },\n  ],\n\n  \"env\": {\n    \"staging\": {\n      \"services\": [\n        { \"binding\": \"APP_SERVICE\", \"service\": \"example-app-staging\" },\n        { \"binding\": \"API_SERVICE\", \"service\": \"example-api-staging\" },\n      ],\n    },\n    \"preview\": {\n      \"services\": [\n        { \"binding\": \"APP_SERVICE\", \"service\": \"example-app-preview\" },\n        { \"binding\": \"API_SERVICE\", \"service\": \"example-api-preview\" },\n      ],\n    },\n  },\n}\n```\n\nWorker naming convention: `<project>-<worker>-<env>`. Production omits the environment suffix.\n\n| Environment | Web                   | App                   | API                   |\n| ----------- | --------------------- | --------------------- | --------------------- |\n| Production  | `example-web`         | `example-app`         | `example-api`         |\n| Staging     | `example-web-staging` | `example-app-staging` | `example-api-staging` |\n| Preview     | `example-web-preview` | `example-app-preview` | `example-api-preview` |\n\n## Hyperdrive\n\n[Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) provides connection pooling between Workers and Neon PostgreSQL. The API worker declares two bindings per environment:\n\n| Binding             | Caching  | Purpose                                |\n| ------------------- | -------- | -------------------------------------- |\n| `HYPERDRIVE_CACHED` | Enabled  | Read-heavy queries                     |\n| `HYPERDRIVE_DIRECT` | Disabled | Writes and consistency-sensitive reads |\n\n```jsonc\n// apps/api/wrangler.jsonc\n\"hyperdrive\": [\n  { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-hyperdrive-cached-id-here\" },\n  { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-hyperdrive-direct-id-here\" }\n]\n```\n\nEach environment has its own Hyperdrive IDs pointing to the corresponding Neon database branch.\n\nThe connection code in `apps/api/lib/db.ts`:\n\n```ts\nimport { schema } from \"@repo/db\";\nimport { drizzle } from \"drizzle-orm/postgres-js\";\nimport postgres from \"postgres\";\n\nexport function createDb(db: Hyperdrive) {\n  const client = postgres(db.connectionString, {\n    max: 1, // Workers are single-request; one connection is enough\n    prepare: false, // Avoids prepared statement caching issues in Workers\n    connect_timeout: 10,\n    idle_timeout: 20,\n    max_lifetime: 60 * 30,\n    transform: { undefined: null },\n    onnotice: () => {}, // Suppress PostgreSQL NOTICE messages\n  });\n  return drizzle(client, { schema, casing: \"snake_case\" });\n}\n```\n\nKey settings: `max: 1` because each Worker invocation handles a single request. `prepare: false` prevents issues with Hyperdrive's connection reuse where prepared statements from a previous request may not exist on the pooled connection.\n\n## Static Assets\n\n### Web Worker\n\nThe web worker serves marketing pages from `apps/web/dist/`. The `run_worker_first` setting forces specific paths through the worker script before falling back to static assets:\n\n```jsonc\n// apps/web/wrangler.jsonc\n\"assets\": {\n  \"directory\": \"./dist\",\n  \"binding\": \"ASSETS\",\n  \"run_worker_first\": [\"/\"]\n}\n```\n\nThis is required for the `/` route where the worker checks the auth hint cookie to decide between the marketing page and the app dashboard. All other paths either match explicit worker routes (`/api/*`, `/login*`) or fall through to static assets.\n\n### App Worker\n\nThe app worker is a pure static asset worker with SPA fallback – no custom worker script:\n\n```jsonc\n// apps/app/wrangler.jsonc\n\"assets\": {\n  \"directory\": \"./dist\",\n  \"not_found_handling\": \"single-page-application\"\n}\n```\n\n`not_found_handling: \"single-page-application\"` returns `index.html` for any path that doesn't match a static file, enabling TanStack Router's client-side routing.\n\n## Auth Hint Cookie Routing\n\nThe web worker's `/` route uses the auth hint cookie to choose between two upstream workers:\n\n```ts\n// apps/web/worker.ts\napp.on([\"GET\", \"HEAD\"], \"/\", async (c) => {\n  const hasAuthHint =\n    getCookie(c, \"__Host-auth\") === \"1\" || getCookie(c, \"auth\") === \"1\";\n\n  const upstream = await (hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS).fetch(\n    c.req.raw,\n  );\n\n  // Prevent caching – response varies by auth state\n  const headers = new Headers(upstream.headers);\n  headers.set(\"Cache-Control\", \"private, no-store\");\n  headers.set(\"Vary\", \"Cookie\");\n\n  return new Response(upstream.body, {\n    status: upstream.status,\n    statusText: upstream.statusText,\n    headers,\n  });\n});\n```\n\nThe `Cache-Control: private, no-store` and `Vary: Cookie` headers prevent CDN and browser caches from serving the wrong version (marketing page to a logged-in user or vice versa). See [ADR-001](/adr/001-auth-hint-cookie) for the full decision record.\n\n## Infrastructure\n\nWorker metadata and Hyperdrive bindings are provisioned with Terraform. Wrangler handles code deployment and route configuration.\n\n```\ninfra/\n├── stacks/\n│   ├── edge/          # Workers, Hyperdrive, DNS\n│   │   ├── main.tf\n│   │   ├── variables.tf\n│   │   └── outputs.tf\n│   └── hybrid/        # Database and other resources\n├── modules/\n│   ├── cloudflare/    # Worker, Hyperdrive, DNS modules\n│   └── gcp/\n├── envs/              # Per-environment Terraform root modules\n└── templates/\n```\n\nThe edge stack (`infra/stacks/edge/main.tf`) creates all three workers, a Hyperdrive binding pair, and DNS records:\n\n```hcl\nmodule \"worker_api\" {\n  source = \"../../modules/cloudflare/worker\"\n  name   = \"${var.project_slug}-api${local.worker_suffix}\"\n  # ...\n}\n\nmodule \"hyperdrive\" {\n  source       = \"../../modules/cloudflare/hyperdrive\"\n  name         = \"${var.project_slug}-${var.environment}\"\n  database_url = var.neon_database_url\n}\n```\n\nThe `worker_suffix` local resolves to `\"\"` for production and `\"-${var.environment}\"` for other environments, matching the naming convention used in service bindings.\n\n## Local Development\n\n`bun dev` starts all three workers concurrently with Wrangler's dev mode:\n\n| Worker | Port   | Notes                                   |\n| ------ | ------ | --------------------------------------- |\n| web    | `5173` | Entry point – open this in your browser |\n| app    | `5174` | Accessed via service binding from web   |\n| api    | `5175` | Accessed via service binding from web   |\n\nIn development, Wrangler simulates service bindings locally – requests between workers happen in-process rather than over the network. The `dev` environment in each `wrangler.jsonc` provides development-specific variables (`APP_ORIGIN: http://localhost:5173`, etc.).\n\n::: tip\nEmail templates must be built before starting the API dev server. The `bun dev` script handles this automatically by running `bun email:build` first.\n:::\n"
  },
  {
    "path": "docs/architecture/index.md",
    "content": "# Architecture Overview\n\nReact Starter Kit runs on three Cloudflare Workers connected by [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). A single domain receives all traffic – the **web** worker routes each request to the right destination without any cross-worker public URLs.\n\n## Request Flow\n\n```mermaid\nsequenceDiagram\n    participant Browser\n    participant Web as Web Worker\n    participant App as App Worker\n    participant API as API Worker\n    participant DB as Neon PostgreSQL\n\n    Browser->>Web: GET /\n    alt auth-hint cookie present\n        Web->>App: service binding\n        App-->>Web: SPA (dashboard)\n    else no cookie\n        Web-->>Browser: marketing page\n    end\n\n    Browser->>Web: GET /settings\n    Web->>App: service binding\n    App-->>Web: SPA assets\n\n    Browser->>Web: POST /api/trpc/user.me\n    Web->>API: service binding\n    API->>DB: Hyperdrive\n    DB-->>API: query result\n    API-->>Web: JSON response\n    Web-->>Browser: JSON response\n```\n\n## Workers\n\n| Worker  | Workspace  | Purpose                                               | Has `nodejs_compat` |\n| ------- | ---------- | ----------------------------------------------------- | :-----------------: |\n| **web** | `apps/web` | Edge router – receives all traffic, routes to app/api |         No          |\n| **app** | `apps/app` | SPA static assets (React, TanStack Router)            |         No          |\n| **api** | `apps/api` | Hono server – tRPC, Better Auth, webhooks             |         Yes         |\n\n### Web Worker\n\nThe web worker is the only worker with a public route (`example.com/*`). It decides where each request goes:\n\n- `/api/*` – forwarded to the API worker\n- `/login`, `/signup`, `/settings`, `/analytics`, `/reports`, `/_app/*` – forwarded to the app worker\n- `/` – routed by [auth hint cookie](#auth-hint-cookie) (app if signed in, marketing site if not)\n- Everything else – served from the web worker's own static assets (marketing pages)\n\n```ts\n// apps/web/worker.ts (simplified)\napp.all(\"/api/*\", (c) => c.env.API_SERVICE.fetch(c.req.raw));\napp.all(\"/login*\", (c) => c.env.APP_SERVICE.fetch(c.req.raw));\n\napp.on([\"GET\", \"HEAD\"], \"/\", async (c) => {\n  const hasAuthHint =\n    getCookie(c, \"__Host-auth\") === \"1\" || getCookie(c, \"auth\") === \"1\";\n  const upstream = await (hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS).fetch(\n    c.req.raw,\n  );\n  // ...\n});\n```\n\n### App Worker\n\nA static asset worker with `not_found_handling: \"single-page-application\"` – any path that doesn't match a file returns `index.html`, enabling client-side routing via TanStack Router.\n\nThe app worker has no custom worker script. It is accessed only through service bindings from the web worker.\n\n### API Worker\n\nRuns the Hono HTTP server with the following middleware chain:\n\n```ts\n// apps/api/worker.ts (simplified)\nworker.onError(errorHandler);\nworker.notFound(notFoundHandler);\nworker.use(secureHeaders());\nworker.use(requestId({ generator: requestIdGenerator }));\nworker.use(logger());\n\n// Initialize shared context\nworker.use(async (c, next) => {\n  const db = createDb(c.env.HYPERDRIVE_CACHED);\n  c.set(\"db\", db);\n  c.set(\"dbDirect\", createDb(c.env.HYPERDRIVE_DIRECT));\n  c.set(\"auth\", createAuth(db, c.env));\n  await next();\n});\n\nworker.route(\"/\", app); // Mounts tRPC + auth + health routes\n```\n\nPrimary endpoints:\n\n| Path          | Handler                                                |\n| ------------- | ------------------------------------------------------ |\n| `/api/auth/*` | Better Auth (login, signup, sessions, OAuth callbacks) |\n| `/api/trpc/*` | tRPC procedures (batching enabled)                     |\n| `/api`        | API info (name, version, endpoint list)                |\n| `/health`     | Health check                                           |\n\n## Service Bindings\n\nService bindings let workers call each other directly over Cloudflare's internal network – no HTTP round-trip through the public internet.\n\n```jsonc\n// apps/web/wrangler.jsonc\n\"services\": [\n  { \"binding\": \"APP_SERVICE\", \"service\": \"example-app\" },\n  { \"binding\": \"API_SERVICE\", \"service\": \"example-api\" }\n]\n```\n\n::: warning\nService bindings are **non-inheritable** in Wrangler – they must be declared in every environment block. Forgetting this causes staging/preview workers to bind to production services.\n:::\n\nNaming convention: `<project>-<worker>-<env>` (e.g. `example-api-staging`). See [Edge > Service Bindings](./edge#service-bindings) for the full per-environment config.\n\n## Database Connection\n\nThe API worker connects to [Neon PostgreSQL](https://neon.tech) via [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) – a connection pool that sits between Workers and your database.\n\nTwo bindings are available:\n\n| Binding             | Caching  | Use case                              |\n| ------------------- | -------- | ------------------------------------- |\n| `HYPERDRIVE_CACHED` | Enabled  | Default reads – most queries go here  |\n| `HYPERDRIVE_DIRECT` | Disabled | Writes and reads that need fresh data |\n\nBoth bindings are initialized in the API worker middleware and available on every request context as `db` and `dbDirect`. See [Database](/database/) for schema and query patterns.\n\n## Auth Hint Cookie\n\nThe `/` route serves two different experiences – a marketing page for visitors and the app dashboard for signed-in users. The web worker needs a fast signal to choose without owning auth logic.\n\n**How it works:** Better Auth sets a lightweight `__Host-auth=1` cookie on sign-in and clears it on sign-out. The web worker checks only for cookie _presence_ – it never validates sessions. If the cookie exists, the request goes to the app worker; otherwise it serves the marketing page.\n\nThis cookie is a **routing hint only**, not a security boundary. A false positive (stale cookie) results in one extra redirect to `/login` – the app worker validates the real session.\n\n::: info\nIn local development the cookie is named `auth` (HTTP), since browsers reject the `__Host-` prefix without HTTPS.\n:::\n\nSee [ADR-001](/adr/001-auth-hint-cookie) for the full decision record and [Sessions & Protected Routes](/auth/sessions) for the auth flow.\n\n## Environments\n\n| Environment | Workers         | Domain                | Database       | Deploy command                  |\n| ----------- | --------------- | --------------------- | -------------- | ------------------------------- |\n| Development | `wrangler dev`  | `localhost:5173`      | Dev branch     | `bun dev`                       |\n| Preview     | `*-preview`     | `preview.example.com` | Preview branch | `wrangler deploy --env preview` |\n| Staging     | `*-staging`     | `staging.example.com` | Staging branch | `wrangler deploy --env staging` |\n| Production  | `*` (no suffix) | `example.com`         | Main branch    | `wrangler deploy`               |\n\nEach environment has its own Hyperdrive bindings, service binding targets, and `APP_ORIGIN` / `ALLOWED_ORIGINS` variables. See [Edge > Service Bindings](./edge#service-bindings) for the full wrangler config.\n\n## Build Order\n\nThe workspaces must build in dependency order:\n\n```\nemail → web → api → app\n```\n\nEmail templates are compiled first because the API server imports them. The `bun build` command handles this automatically.\n\n## Key Invariants\n\n- The **API worker is the sole authority** for authentication and data access – the web worker never validates sessions or queries the database.\n- Only the **web worker** has public routes. App and API workers are accessed exclusively through service bindings.\n- **Service bindings are non-inheritable** – every Wrangler environment must declare its own bindings.\n- The auth hint cookie is a **routing optimization**, not a security mechanism.\n- The API worker is the only worker with `nodejs_compat` enabled.\n"
  },
  {
    "path": "docs/auth/email-otp.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Email & OTP\n\nThe primary sign-in method is passwordless email OTP. Users enter their email, receive a 6-digit code, and enter it to authenticate. The same flow handles both login and signup – if the email doesn't exist, Better Auth creates the account automatically.\n\n## Server Configuration\n\nThe `emailOTP` plugin is configured in `apps/api/lib/auth.ts`:\n\n```ts\nemailOTP({\n  async sendVerificationOTP({ email, otp, type }) {\n    await sendOTP(env, { email, otp, type });\n  },\n  otpLength: 6,\n  expiresIn: 300,      // 5 minutes\n  allowedAttempts: 3,   // max wrong guesses before code is invalidated\n}),\n```\n\nOTP codes are stored in the `verification` table and automatically expire. After 3 failed attempts, the code is invalidated and the user must request a new one.\n\n### Email Delivery\n\nOTP emails are sent via [React Email](https://react.email/) templates rendered to HTML + plain text, delivered through [Resend](https://resend.com/):\n\n```ts\n// apps/api/lib/email.ts\nexport async function sendOTP(env, { email, otp, type }) {\n  // In development, OTP is also printed to the console\n  if (env.ENVIRONMENT === \"development\") {\n    console.log(`OTP code for ${email}: ${otp}`);\n  }\n\n  const component = OTPEmail({ otp, type, appName: env.APP_NAME });\n  const html = await renderEmailToHtml(component);\n  const text = await renderEmailToText(component);\n\n  return sendEmail(env, {\n    to: email,\n    subject: `Your Sign In code`,\n    html,\n    text,\n  });\n}\n```\n\n::: tip\nDuring local development, OTP codes are logged to the terminal – you don't need a real Resend API key to test the flow.\n:::\n\n## Client Flow\n\nThe auth form implements a 3-step state machine:\n\n```\nmethod → email → otp\n```\n\nEach step is a separate UI component orchestrated by `AuthForm`:\n\n| Step     | Component         | What Happens                                       |\n| -------- | ----------------- | -------------------------------------------------- |\n| `method` | `MethodSelection` | User picks sign-in method (Google, email, passkey) |\n| `email`  | `EmailInput`      | User enters email, OTP is sent                     |\n| `otp`    | `OtpVerification` | User enters 6-digit code to complete sign-in       |\n\n### State Machine\n\nThe state transitions are defined in `apps/app/components/auth/use-auth-form.ts`:\n\n```ts\nconst VALID_TRANSITIONS: Record<AuthStep, AuthStep[]> = {\n  method: [\"email\"],\n  email: [\"method\", \"otp\"],\n  otp: [\"email\"],\n};\n```\n\nTransitions are validated – invalid step jumps are silently ignored. This prevents race conditions from concurrent auth operations (e.g., passkey conditional UI completing while the user clicks a button).\n\n### Sending the OTP\n\nWhen the user submits their email, the `sendOtp` function normalizes the input and calls the Better Auth client:\n\n```ts\n// \"sign-in\" type handles both login and signup\nconst result = await auth.emailOtp.sendVerificationOtp({\n  email: normalizedEmail,\n  type: \"sign-in\",\n});\n```\n\nThe `sign-in` type is used for both login and signup flows. Better Auth creates the user account if the email is new.\n\n### Verifying the Code\n\nThe `OtpVerification` component handles code entry and verification:\n\n```ts\nconst result = await auth.signIn.emailOtp({ email, otp });\n```\n\nThe input field restricts to 6 numeric digits with `inputMode=\"numeric\"` and `autoComplete=\"one-time-code\"` for mobile OTP autofill.\n\n## Error Handling\n\nThe OTP plugin returns specific error codes that map to user-friendly messages:\n\n| Error Code          | User Message                                           | Behavior                      |\n| ------------------- | ------------------------------------------------------ | ----------------------------- |\n| `TOO_MANY_ATTEMPTS` | \"Too many failed attempts. Please request a new code.\" | Returns to email step         |\n| `OTP_EXPIRED`       | \"Code has expired. Please request a new one.\"          | Returns to email step         |\n| `INVALID_OTP`       | Server message or \"Invalid verification code\" fallback | Stays on OTP step (can retry) |\n\nWhen `TOO_MANY_ATTEMPTS` or `OTP_EXPIRED` occurs, the form automatically returns to the email step so the user can request a fresh code.\n\n### Resend Cooldown\n\nAfter the initial OTP is sent, users can request a new code with a 30-second cooldown:\n\n```ts\nconst RESEND_COOLDOWN_SECONDS = 30;\n```\n\nThe resend button shows a countdown timer and is disabled during the cooldown period.\n\n## Component Architecture\n\n```\nAuthForm\n├── MethodSelection          Step 1: choose sign-in method\n│   ├── GoogleLogin          OAuth redirect\n│   ├── \"Continue with email\" button\n│   └── PasskeyLogin         WebAuthn (login only)\n├── EmailInput               Step 2: enter email, send OTP\n└── OtpStep                  Step 3: wraps OTP UI with back link (internal to AuthForm)\n    └── OtpVerification      Code entry and verification\n```\n\nThe `AuthForm` accepts a `mode` prop (`\"login\"` or `\"signup\"`) that controls copy and available methods. Both modes use the same OTP flow – the difference is cosmetic (headings, ToS display, passkey availability).\n\n::: info\nPasskeys are only shown during login. They require an existing account with a registered passkey – see [Passkeys](./passkeys).\n:::\n"
  },
  {
    "path": "docs/auth/index.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Authentication Overview\n\nAuthentication is handled by [Better Auth](https://www.better-auth.com/) – a TypeScript-native auth framework that runs entirely in the API worker. The project ships with multiple sign-in methods, organization-based multi-tenancy, and Stripe billing integration out of the box.\n\n## What's Included\n\n| Method                             | Description                               |\n| ---------------------------------- | ----------------------------------------- |\n| [Email & OTP](./email-otp)         | Passwordless 6-digit code via email       |\n| Email & Password                   | Traditional email/password with reset     |\n| [Google OAuth](./social-providers) | Social login with redirect flow           |\n| [Passkeys](./passkeys)             | WebAuthn biometric / security key         |\n| Anonymous                          | Guest sessions that can be upgraded later |\n\nAll methods produce the same session format. Users can link multiple methods to one account.\n\n## Plugins\n\nBetter Auth's functionality is extended through plugins. The server and client must enable matching plugins:\n\n| Plugin         | Server           | Client                 | Purpose                     |\n| -------------- | ---------------- | ---------------------- | --------------------------- |\n| `emailOTP`     | `emailOTP()`     | `emailOTPClient()`     | Passwordless OTP sign-in    |\n| `organization` | `organization()` | `organizationClient()` | Multi-tenant orgs and roles |\n| `passkey`      | `passkey()`      | `passkeyClient()`      | WebAuthn authentication     |\n| `anonymous`    | `anonymous()`    | `anonymousClient()`    | Guest sessions              |\n| `stripe`       | `stripe()`       | `stripeClient()`       | Subscription billing        |\n\nThe Stripe plugin is conditionally loaded – it only activates when `STRIPE_SECRET_KEY` and related env vars are set. Without them, the app works normally but billing endpoints return 404.\n\n## Server Configuration\n\nThe auth instance is created per-request in `apps/api/lib/auth.ts`:\n\n```ts\n// apps/api/lib/auth.ts\nexport function createAuth(db: DB, env: AuthEnv) {\n  return betterAuth({\n    baseURL: `${env.APP_ORIGIN}/api/auth`,\n    trustedOrigins: [env.APP_ORIGIN],\n    secret: env.BETTER_AUTH_SECRET,\n    database: drizzleAdapter(db, { provider: \"pg\", schema: { ... } }),\n\n    emailAndPassword: {\n      enabled: true,\n      sendResetPassword: async ({ user, url }) => {\n        await sendPasswordReset(env, { user, url });\n      },\n    },\n\n    emailVerification: {\n      sendVerificationEmail: async ({ user, url }) => {\n        await sendVerificationEmail(env, { user, url });\n      },\n    },\n\n    socialProviders: {\n      google: {\n        clientId: env.GOOGLE_CLIENT_ID,\n        clientSecret: env.GOOGLE_CLIENT_SECRET,\n      },\n    },\n\n    plugins: [\n      anonymous(),\n      organization({\n        allowUserToCreateOrganization: true,\n        organizationLimit: 5,\n        creatorRole: \"owner\",\n      }),\n      passkey({ rpID, rpName: env.APP_NAME, origin: env.APP_ORIGIN }),\n      emailOTP({ otpLength: 6, expiresIn: 300, allowedAttempts: 3 }),\n      ...stripePlugin(db, env),\n    ],\n  });\n}\n```\n\nThe `account` model is renamed to `identity` to better describe its purpose (OAuth provider credentials):\n\n```ts\naccount: { modelName: \"identity\" },\n```\n\n### ID Generation\n\nAll auth tables use prefixed CUID2 IDs generated at the application level:\n\n```ts\nadvanced: {\n  database: {\n    generateId: ({ model }) => generateAuthId(model),\n  },\n},\n```\n\nThis produces IDs like `usr_cm...`, `ses_cm...`, `org_cm...` – making it easy to identify what kind of record an ID refers to.\n\n## Client Configuration\n\nThe auth client lives in `apps/app/lib/auth.ts`:\n\n```ts\n// apps/app/lib/auth.ts\nimport { createAuthClient } from \"better-auth/react\";\n\nexport const auth = createAuthClient({\n  baseURL: baseURL + \"/api/auth\",\n  plugins: [\n    anonymousClient(),\n    emailOTPClient(),\n    organizationClient(),\n    passkeyClient(),\n    stripeClient({ subscription: true }),\n  ],\n});\n```\n\n::: warning\nDo not use `auth.useSession()` directly. Session state is managed exclusively through TanStack Query – see [Sessions & Protected Routes](./sessions).\n:::\n\n## Auth Routes\n\nBetter Auth exposes HTTP endpoints at `/api/auth/*`. These are mounted in the Hono app alongside tRPC:\n\n```\n/api/auth/sign-in/*        Sign-in endpoints (email, social, passkey)\n/api/auth/sign-up/*        Sign-up endpoints\n/api/auth/sign-out         Session termination\n/api/auth/get-session      Current session data\n/api/auth/callback/*       OAuth callbacks\n/api/auth/email-otp/*      OTP send and verify\n/api/auth/passkey/*        WebAuthn registration and authentication\n/api/auth/organization/*   Organization CRUD and membership\n```\n\nSee the [Better Auth API reference](https://www.better-auth.com/docs/api-reference) for the full endpoint list.\n\n## Database Tables\n\nAuthentication uses 9 database tables defined in `db/schema/`:\n\n| Table          | File              | Description                                                |\n| -------------- | ----------------- | ---------------------------------------------------------- |\n| `user`         | `user.ts`         | User accounts with profile info                            |\n| `session`      | `user.ts`         | Active sessions with `activeOrganizationId`                |\n| `identity`     | `user.ts`         | OAuth provider credentials (Better Auth's `account` model) |\n| `verification` | `user.ts`         | Email verification and OTP tokens                          |\n| `organization` | `organization.ts` | Tenant organizations                                       |\n| `member`       | `organization.ts` | Organization memberships with roles                        |\n| `invitation`   | `invitation.ts`   | Pending org invitations                                    |\n| `passkey`      | `passkey.ts`      | WebAuthn credential store                                  |\n| `subscription` | `subscription.ts` | Stripe subscription state                                  |\n\n## Auth Hint Cookie\n\nThe API worker sets a lightweight cookie (`__Host-auth` in HTTPS, `auth` in HTTP dev) on sign-in and clears it on sign-out. The web edge worker reads this cookie to route `/` – authenticated users get the app, anonymous users get the marketing page. This cookie is a routing hint only, not a security boundary. See [ADR-001](/adr/001-auth-hint-cookie) for the full rationale.\n\n## Environment Variables\n\n| Variable               | Required | Description                                       |\n| ---------------------- | -------- | ------------------------------------------------- |\n| `BETTER_AUTH_SECRET`   | Yes      | Secret for signing sessions and tokens            |\n| `GOOGLE_CLIENT_ID`     | Yes      | Google OAuth client ID                            |\n| `GOOGLE_CLIENT_SECRET` | Yes      | Google OAuth client secret                        |\n| `RESEND_API_KEY`       | Yes      | API key for sending OTP emails                    |\n| `RESEND_EMAIL_FROM`    | Yes      | Sender address for auth emails                    |\n| `APP_NAME`             | Yes      | Display name (used in emails and passkey prompts) |\n| `APP_ORIGIN`           | Yes      | Full origin URL (e.g., `https://example.com`)     |\n"
  },
  {
    "path": "docs/auth/organizations.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Organizations & Roles\n\nOrganizations provide multi-tenant isolation. Each organization is a separate tenant with its own members, roles, and billing. Users can belong to multiple organizations and switch between them.\n\n## Server Configuration\n\nThe organization plugin is configured in `apps/api/lib/auth.ts`:\n\n```ts\norganization({\n  allowUserToCreateOrganization: true,\n  organizationLimit: 5,\n  creatorRole: \"owner\",\n}),\n```\n\n| Setting                         | Value     | Description                               |\n| ------------------------------- | --------- | ----------------------------------------- |\n| `allowUserToCreateOrganization` | `true`    | Any user can create organizations         |\n| `organizationLimit`             | `5`       | Max organizations per user                |\n| `creatorRole`                   | `\"owner\"` | Creator automatically gets the owner role |\n\n## Database Tables\n\n### `organization`\n\nDefined in `db/schema/organization.ts`:\n\n| Column             | Type   | Description                           |\n| ------------------ | ------ | ------------------------------------- |\n| `id`               | `text` | Prefixed CUID2 (`org_cm...`)          |\n| `name`             | `text` | Display name                          |\n| `slug`             | `text` | URL-safe unique identifier            |\n| `logo`             | `text` | Logo URL (optional)                   |\n| `metadata`         | `text` | JSON string for custom data           |\n| `stripeCustomerId` | `text` | Stripe customer for org-level billing |\n\n### `member`\n\nLinks users to organizations with a role:\n\n| Column           | Type   | Description                         |\n| ---------------- | ------ | ----------------------------------- |\n| `id`             | `text` | Prefixed CUID2 (`mem_cm...`)        |\n| `userId`         | `text` | References `user.id`                |\n| `organizationId` | `text` | References `organization.id`        |\n| `role`           | `text` | `\"owner\"`, `\"admin\"`, or `\"member\"` |\n\nA unique constraint on `(userId, organizationId)` prevents duplicate memberships.\n\n### `invitation`\n\nManages pending invitations, defined in `db/schema/invitation.ts`:\n\n| Column           | Type        | Description                                              |\n| ---------------- | ----------- | -------------------------------------------------------- |\n| `id`             | `text`      | Prefixed CUID2 (`inv_cm...`)                             |\n| `email`          | `text`      | Invitee's email address                                  |\n| `inviterId`      | `text`      | References `user.id`                                     |\n| `organizationId` | `text`      | References `organization.id`                             |\n| `role`           | `text`      | Role assigned upon acceptance                            |\n| `status`         | `text`      | `\"pending\"`, `\"accepted\"`, `\"rejected\"`, or `\"canceled\"` |\n| `expiresAt`      | `timestamp` | Invitation expiration                                    |\n| `acceptedAt`     | `timestamp` | When the invite was accepted                             |\n| `rejectedAt`     | `timestamp` | When the invite was rejected or canceled                 |\n\nA unique constraint on `(organizationId, email)` prevents duplicate invitations to the same person.\n\n## Roles\n\nThree built-in roles with hierarchical permissions:\n\n| Role       | Can manage members | Can manage settings | Can delete org |\n| ---------- | ------------------ | ------------------- | -------------- |\n| **owner**  | Yes                | Yes                 | Yes            |\n| **admin**  | Yes                | Yes                 | No             |\n| **member** | No                 | No                  | No             |\n\n### Role Checks in API Procedures\n\nUse the session's `activeOrganizationId` with a membership query to check roles:\n\n```ts\n// apps/api/routers/organization.ts\nconst [row] = await ctx.db\n  .select({ role: Db.member.role })\n  .from(Db.member)\n  .where(\n    and(\n      eq(Db.member.organizationId, referenceId),\n      eq(Db.member.userId, user.id),\n    ),\n  );\n\nconst isAdmin = row?.role === \"owner\" || row?.role === \"admin\";\n```\n\n## Active Organization\n\nThe session tracks which organization is currently active via `activeOrganizationId`:\n\n```ts\nexport type AuthSession = SessionResponse[\"session\"] & {\n  activeOrganizationId?: string;\n};\n```\n\nThis field is stored in the `session` table and persists across requests. When the user switches organizations, Better Auth updates this field.\n\n## Billing Integration\n\nSubscriptions scope to the active organization. The billing router uses `activeOrganizationId` as the billing reference, falling back to the user's own ID for personal billing:\n\n```ts\n// apps/api/routers/billing.ts\nconst referenceId = ctx.session.activeOrganizationId ?? ctx.user.id;\n```\n\nThe Stripe plugin's `authorizeReference` hook enforces that only owners and admins can manage an organization's subscription:\n\n```ts\nauthorizeReference: async ({ user, referenceId }) => {\n  if (referenceId === user.id) return true; // Personal billing\n  const [row] = await db\n    .select({ role: Db.member.role })\n    .from(Db.member)\n    .where(\n      and(\n        eq(Db.member.organizationId, referenceId),\n        eq(Db.member.userId, user.id),\n      ),\n    );\n  return row?.role === \"owner\" || row?.role === \"admin\";\n},\n```\n\n## Invitation Lifecycle\n\n1. **Owner/admin invites** – sends invitation to email with assigned role\n2. **Invitation pending** – stored in `invitation` table with `status: \"pending\"` and an expiration\n3. **Invitee accepts** – Better Auth creates a `member` record and updates invitation status\n4. **Or invitee rejects / invitation expires** – invitation status is updated, no member created\n\nEach organization can only have one pending invitation per email address.\n\n## Client API\n\nThe `organizationClient()` plugin adds organization methods to the auth client:\n\n```ts\n// Create an organization\nawait auth.organization.create({ name: \"Acme Inc\", slug: \"acme\" });\n\n// List user's organizations\nconst { data } = await auth.organization.list();\n\n// Set active organization\nawait auth.organization.setActive({ organizationId: \"org_cm...\" });\n\n// Invite a member\nawait auth.organization.inviteMember({\n  email: \"jane@example.com\",\n  role: \"member\",\n  organizationId: \"org_cm...\",\n});\n```\n\nSee the [Better Auth organization plugin docs](https://www.better-auth.com/docs/plugins/organization) for the complete client API.\n"
  },
  {
    "path": "docs/auth/passkeys.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Passkeys\n\nPasskey authentication uses the [WebAuthn](https://webauthn.io/) standard to let users sign in with biometrics (Touch ID, Face ID) or hardware security keys. It's the most secure sign-in method – no shared secrets leave the device.\n\n::: info\nPasskeys are available for **login only**. Users must first create an account via email OTP or Google OAuth, then register a passkey from their account settings. The sign-up form does not show the passkey option.\n:::\n\n## Server Configuration\n\nThe passkey plugin is configured in `apps/api/lib/auth.ts`:\n\n```ts\npasskey({\n  rpID,         // Domain name (e.g., \"example.com\" or \"localhost\")\n  rpName: env.APP_NAME,  // Human-readable name shown in browser prompts\n  origin: env.APP_ORIGIN,\n}),\n```\n\nThe `rpID` (Relying Party ID) is extracted from `APP_ORIGIN`:\n\n```ts\nconst appUrl = new URL(env.APP_ORIGIN);\nconst rpID = appUrl.hostname;\n```\n\nThis means passkeys are bound to the domain – a passkey registered on `example.com` won't work on `staging.example.com`. The `rpName` appears in the browser's passkey dialog (e.g., \"Sign in to My App\").\n\n### Database Table\n\nPasskey credentials are stored in `db/schema/passkey.ts`:\n\n| Column         | Description                                             |\n| -------------- | ------------------------------------------------------- |\n| `publicKey`    | WebAuthn public key                                     |\n| `credentialID` | Unique credential identifier                            |\n| `counter`      | Signature counter (replay protection)                   |\n| `deviceType`   | `\"singleDevice\"` or `\"multiDevice\"`                     |\n| `backedUp`     | Whether the credential is synced across devices         |\n| `transports`   | Communication methods (USB, BLE, NFC, internal)         |\n| `deviceName`   | User-friendly label (e.g., \"MacBook Pro\")               |\n| `platform`     | `\"platform\"` (built-in) or `\"cross-platform\"` (USB key) |\n\n## Client Component\n\nThe `PasskeyLogin` component in `apps/app/components/auth/passkey-login.tsx` handles two modes:\n\n### Explicit Login\n\nWhen the user clicks \"Log in with passkey\", the component checks for WebAuthn support and triggers the browser's credential picker:\n\n```ts\nconst handlePasskeyLogin = async () => {\n  if (!window.PublicKeyCredential) {\n    onError(authConfig.errors.passkeyNotSupported);\n    return;\n  }\n\n  const result = await auth.signIn.passkey();\n\n  if (result.data) {\n    onSuccess();\n  } else if (result.error) {\n    const errorCode = \"code\" in result.error ? result.error.code : undefined;\n    if (errorCode === \"AUTH_CANCELLED\") {\n      onError(\"Passkey authentication was cancelled.\");\n    } else {\n      onError(result.error.message || authConfig.errors.genericError);\n    }\n  }\n};\n```\n\n### Conditional UI (Autofill)\n\nWhen enabled, passkey autofill shows saved credentials in the browser's autocomplete dropdown – similar to how password managers work. This runs passively on mount:\n\n```ts\nuseEffect(() => {\n  if (!authConfig.passkey.enableConditionalUI) return;\n\n  const setupConditionalUI = async () => {\n    if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return;\n\n    const isAvailable =\n      await window.PublicKeyCredential.isConditionalMediationAvailable();\n    if (!isAvailable) return;\n\n    const result = await auth.signIn.passkey({ autoFill: true });\n    if (result.data && !aborted) {\n      onSuccessRef.current();\n    }\n  };\n\n  setupConditionalUI();\n}, []);\n```\n\nConditional UI is controlled by the `authConfig.passkey.enableConditionalUI` flag (default: `true`). Errors from conditional UI are silently ignored since the user hasn't explicitly requested authentication.\n\n## Client Configuration\n\nPasskey behavior is configured in `apps/app/lib/auth-config.ts`:\n\n```ts\npasskey: {\n  enableConditionalUI: true,\n  timeout: 60_000,          // 60 seconds for user interaction\n  userVerification: \"preferred\",\n},\n```\n\n| Setting               | Default       | Description                                                 |\n| --------------------- | ------------- | ----------------------------------------------------------- |\n| `enableConditionalUI` | `true`        | Show passkeys in browser autocomplete                       |\n| `timeout`             | `60000`       | Max time (ms) for user to interact with the WebAuthn dialog |\n| `userVerification`    | `\"preferred\"` | Request biometric/PIN when available, but don't require it  |\n\n## Error Handling\n\n| Error                 | Cause                                              | Behavior                      |\n| --------------------- | -------------------------------------------------- | ----------------------------- |\n| `AUTH_CANCELLED`      | User dismissed the WebAuthn prompt or it timed out | Shows cancellation message    |\n| `passkeyNotSupported` | `window.PublicKeyCredential` is undefined          | Shows browser support message |\n| Network error         | Offline or DNS failure                             | Shows network error message   |\n| Server error          | No passkey found, invalid credential               | Shows server error message    |\n\n## Browser Support\n\nPasskeys require WebAuthn support. All modern browsers support it:\n\n- Chrome 67+, Edge 18+, Firefox 60+, Safari 13+\n- iOS 16+ (synced via iCloud Keychain)\n- Android 9+ (synced via Google Password Manager)\n\nThe component checks `window.PublicKeyCredential` before attempting authentication and shows a clear message on unsupported browsers.\n"
  },
  {
    "path": "docs/auth/sessions.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Sessions & Protected Routes\n\nSession state is managed exclusively through TanStack Query. The auth client fetches session data, TanStack Query caches it, and route guards use the cache to protect pages – no direct `auth.useSession()` calls or local storage.\n\n## Session Query\n\nThe session query is defined in `apps/app/lib/queries/session.ts`:\n\n```ts\nexport function sessionQueryOptions() {\n  return queryOptions<SessionData | null>({\n    queryKey: [\"auth\", \"session\"],\n    queryFn: async () => {\n      const response = await auth.getSession();\n      if (response.error) throw response.error;\n      return response.data;\n    },\n    staleTime: 30_000, // 30 seconds\n    retry(failureCount, error) {\n      const status = getErrorStatus(error);\n      if (status === 401 || status === 403) return false;\n      return failureCount < 3;\n    },\n  });\n}\n```\n\nKey behaviors:\n\n- Returns `null` when unauthenticated (not an error)\n- 30-second stale time keeps auth state current without excessive requests\n- 401/403 errors are not retried – retrying won't help for auth failures\n- Inherits global `gcTime`, `refetchOnWindowFocus`, and `refetchOnReconnect` from QueryClient defaults\n\n### Session Data Shape\n\n```ts\ninterface SessionData {\n  user: User; // id, name, email, emailVerified, image, ...\n  session: Session; // id, token, expiresAt, activeOrganizationId, ...\n}\n```\n\nBoth `user` and `session` must be present for valid auth state. Partial data (only user, only session) is treated as unauthenticated.\n\n### Reading Session Data\n\n```ts\n// In components – triggers fetch if stale\nconst { data } = useSessionQuery();\n\n// With Suspense\nconst { data } = useSuspenseSessionQuery();\n\n// Sync check of cache only – no network request\nconst session = getCachedSession(queryClient);\nconst loggedIn = isAuthenticated(queryClient);\n```\n\n## Protected Route Guard\n\nThe `(app)/route.tsx` layout route protects all app pages with a cache-first auth check:\n\n```ts\n// apps/app/routes/(app)/route.tsx\nexport const Route = createFileRoute(\"/(app)\")({\n  beforeLoad: async ({ context, location }) => {\n    // Check cache first – instant navigation if session is cached\n    let session = getCachedSession(context.queryClient);\n\n    // Fetch only if cache is empty (first load or after cache clear)\n    if (session === undefined) {\n      session = await context.queryClient.fetchQuery(sessionQueryOptions());\n    }\n\n    if (!session?.user || !session?.session) {\n      throw redirect({\n        to: \"/login\",\n        search: { returnTo: location.href },\n      });\n    }\n\n    return { user: session.user, session };\n  },\n  component: AppLayout,\n});\n```\n\nThis pattern means:\n\n- **Cached session** → navigation is instant (no network request)\n- **No cache** → fetches session, redirects to `/login` if unauthenticated\n- **`returnTo`** → preserves the original URL so users land back after login\n\nThe session data is returned from `beforeLoad` and available to all child routes via `Route.useRouteContext()`.\n\n## Login Page\n\nThe login route (`(auth)/login.tsx`) handles the inverse – redirecting authenticated users away:\n\n```ts\n// apps/app/routes/(auth)/login.tsx\nexport const Route = createFileRoute(\"/(auth)/login\")({\n  validateSearch: searchSchema,\n  beforeLoad: async ({ context, search }) => {\n    try {\n      const session = await context.queryClient.fetchQuery(\n        sessionQueryOptions(),\n      );\n      if (session?.user && session?.session) {\n        throw redirect({ to: search.returnTo ?? \"/\" });\n      }\n    } catch (error) {\n      if (isRedirect(error)) throw error;\n      // Fetch errors → show login form\n    }\n  },\n});\n```\n\nAfter successful authentication, the login page revalidates the session and navigates:\n\n```ts\nasync function handleSuccess() {\n  await revalidateSession(queryClient, router);\n  await router.navigate({ to: search.returnTo ?? \"/\" });\n}\n```\n\n`revalidateSession` removes the cached session (forcing a fresh fetch) and invalidates the router so `beforeLoad` re-runs with new data.\n\n## Sign Out\n\nThe `signOut` function clears the server session, updates the cache, and performs a hard redirect:\n\n```ts\n// apps/app/lib/queries/session.ts\nexport async function signOut(\n  queryClient: QueryClient,\n  options?: { redirect?: boolean },\n) {\n  try {\n    await auth.signOut();\n  } finally {\n    queryClient.setQueryData(sessionQueryKey, null);\n\n    if (options?.redirect !== false) {\n      window.location.href = \"/login\";\n    }\n  }\n}\n```\n\nThe hard redirect (`window.location.href`) resets all in-memory state – Jotai atoms, component state, TanStack Query cache – ensuring a clean slate between user sessions. Pass `{ redirect: false }` for programmatic sign-out without navigation. `setQueryData(null)` is used instead of `invalidateQueries` to avoid a wasted refetch of a session that no longer exists.\n\n## Auth Error Boundary\n\nThe `AuthErrorBoundary` wraps protected route layouts and catches authentication errors that occur during rendering (e.g., a tRPC call returns 401):\n\n```ts\n// apps/app/components/auth/auth-error-boundary.tsx\nexport function AuthErrorBoundary({ children }) {\n  return (\n    <ErrorBoundary\n      FallbackComponent={AuthAwareErrorFallback}\n      onError={(error) => {\n        if (isUnauthenticatedError(error)) {\n          queryClient.removeQueries({ queryKey: sessionQueryKey });\n        }\n      }}\n    >\n      {children}\n    </ErrorBoundary>\n  );\n}\n```\n\nThe fallback UI shows two options:\n\n- **Try Again** – resets the error boundary and refetches the session\n- **Sign In** – clears session cache and redirects to `/login` with `returnTo`\n\nAuth errors (401) get the auth-specific fallback. Other errors (500, network) get a generic error fallback with a retry button.\n\n## Auth Hint Cookie\n\nThe API worker manages a lightweight routing cookie alongside the session. On sign-in, it sets `__Host-auth=1` (HTTPS) or `auth=1` (HTTP dev). On sign-out or invalid session, it clears it.\n\nThe web edge worker reads this cookie to decide how to route `/`:\n\n```ts\n// apps/web/worker.ts\nconst hasAuthHint =\n  getCookie(c, \"__Host-auth\") === \"1\" || getCookie(c, \"auth\") === \"1\";\n\nconst upstream = hasAuthHint ? c.env.APP_SERVICE : c.env.ASSETS;\n```\n\nThis cookie is **not a security boundary** – it's a performance optimization. False positives (stale cookie after session expiry) cause one extra redirect to `/login`. The app worker is always the authority for session validation.\n\nThe cookie lifecycle is managed by Better Auth hooks:\n\n| Event                                 | Action             |\n| ------------------------------------- | ------------------ |\n| New session (sign-in, sign-up, OAuth) | Set cookie         |\n| Sign-out                              | Clear cookie       |\n| Session check with no valid session   | Clear stale cookie |\n\nSee [ADR-001](/adr/001-auth-hint-cookie) for the design rationale.\n"
  },
  {
    "path": "docs/auth/social-providers.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Social Providers\n\nGoogle OAuth is configured out of the box. The flow redirects users to Google's consent screen, then back to your app where Better Auth creates or links the account.\n\n## Server Configuration\n\nGoogle OAuth credentials are set in `apps/api/lib/auth.ts`:\n\n```ts\nsocialProviders: {\n  google: {\n    clientId: env.GOOGLE_CLIENT_ID,\n    clientSecret: env.GOOGLE_CLIENT_SECRET,\n  },\n},\n```\n\n### Setting Up Google OAuth\n\n1. Go to the [Google Cloud Console](https://console.cloud.google.com/apis/credentials)\n2. Create an OAuth 2.0 Client ID (Web application type)\n3. Add authorized redirect URI: `https://your-domain.com/api/auth/callback/google`\n   - For local development: `http://localhost:5173/api/auth/callback/google`\n4. Copy the client ID and secret to your `.env.local`:\n\n```sh\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret\n```\n\n## Client Component\n\nThe `GoogleLogin` component in `apps/app/components/auth/google-login.tsx` handles the OAuth redirect:\n\n```ts\nconst handleGoogleLogin = async () => {\n  // Clear stale session before OAuth redirect\n  queryClient.removeQueries({ queryKey: sessionQueryKey });\n\n  // OAuth redirects to /login which validates session and redirects to returnTo\n  const callbackURL = returnTo\n    ? `/login?returnTo=${encodeURIComponent(returnTo)}`\n    : \"/login\";\n\n  const result = await auth.signIn.social({\n    provider: \"google\",\n    callbackURL,\n  });\n};\n```\n\nThe flow works as follows:\n\n1. User clicks \"Continue with Google\"\n2. Stale session cache is cleared (prevents showing old data after redirect)\n3. `auth.signIn.social()` redirects to Google's consent screen\n4. After consent, Google redirects back to `/api/auth/callback/google`\n5. Better Auth creates/links the account and sets the session cookie\n6. The callback redirects to `callbackURL` (`/login?returnTo=...`)\n7. The login page detects the active session and redirects to `returnTo`\n\n### Preserving Return URL\n\nThe `returnTo` parameter survives the OAuth round-trip by being encoded into the `callbackURL`. When the user lands back on `/login`, the search params schema validates and sanitizes the URL:\n\n```ts\nconst searchSchema = z.object({\n  returnTo: z\n    .string()\n    .optional()\n    .transform((val) => {\n      const safe = getSafeRedirectUrl(val);\n      return safe === \"/\" ? undefined : safe;\n    })\n    .catch(undefined),\n});\n```\n\nOnly same-origin relative paths are accepted – absolute URLs and protocol-relative URLs (`//evil.com`) are rejected.\n\n## Adding Another Provider\n\nBetter Auth supports [30+ OAuth providers](https://www.better-auth.com/docs/concepts/oauth). To add one:\n\n**1. Add server config** in `apps/api/lib/auth.ts`:\n\n```ts\nsocialProviders: {\n  google: { ... },\n  github: {  // [!code ++]\n    clientId: env.GITHUB_CLIENT_ID,  // [!code ++]\n    clientSecret: env.GITHUB_CLIENT_SECRET,  // [!code ++]\n  },  // [!code ++]\n},\n```\n\n**2. Add env vars** to `apps/api/lib/env.ts` and your `.env.local`.\n\n**3. Update the providers list** in `apps/app/lib/auth-config.ts`:\n\n```ts\noauth: {\n  providers: [\"google\", \"github\"] as const,  // [!code ++]\n},\n```\n\n**4. Create a login button component** following the `GoogleLogin` pattern – clear session cache, call `auth.signIn.social({ provider: \"github\" })`, handle errors.\n\n**5. Add the button** to the `MethodSelection` component in `auth-form.tsx`.\n"
  },
  {
    "path": "docs/billing/checkout.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Checkout Flow\n\nUpgrades and subscription management use Stripe's hosted pages – Stripe Checkout for new subscriptions and the Customer Portal for changes. No Stripe.js client dependency is needed.\n\n## Upgrade Flow\n\nThe auth client handles the redirect to Stripe Checkout:\n\n```ts\n// apps/app/routes/(app)/settings.tsx\nasync function handleUpgrade(plan: \"starter\" | \"pro\") {\n  await auth.subscription.upgrade({\n    plan,\n    successUrl: returnUrl,\n    cancelUrl: returnUrl,\n  });\n}\n```\n\n`auth.subscription.upgrade()` calls the Better Auth endpoint, which creates a Stripe Checkout session and redirects the browser. After payment, Stripe redirects back to `successUrl`. The subscription is activated asynchronously via [webhook](./webhooks).\n\nFor the Pro plan, if `STRIPE_PRO_ANNUAL_PRICE_ID` is configured, Stripe Checkout shows both monthly and annual options automatically.\n\n## Customer Portal\n\nExisting subscribers manage their subscription (cancel, change payment method, switch plans) through Stripe's hosted portal:\n\n```ts\n// apps/app/routes/(app)/settings.tsx\nasync function handleManageBilling() {\n  await auth.subscription.billingPortal({ returnUrl });\n}\n```\n\nConfigure the portal appearance and allowed actions in the [Stripe Dashboard → Customer Portal settings](https://dashboard.stripe.com/settings/billing/portal).\n\n## Authorization\n\nThe plugin's `authorizeReference` callback controls who can manage billing:\n\n| Context                  | Who can upgrade/manage |\n| ------------------------ | ---------------------- |\n| Personal (no active org) | The user themselves    |\n| Organization             | Org owner or admin     |\n\n```ts\n// apps/api/lib/auth.ts\nauthorizeReference: async ({ user, referenceId }) => {\n  // Personal billing\n  if (referenceId === user.id) return true;\n  // Org billing: check membership role\n  const [row] = await db\n    .select({ role: Db.member.role })\n    .from(Db.member)\n    .where(\n      and(\n        eq(Db.member.organizationId, referenceId),\n        eq(Db.member.userId, user.id),\n      ),\n    );\n  return row?.role === \"owner\" || row?.role === \"admin\";\n},\n```\n\nRegular org members see the billing status but cannot modify the subscription.\n\n## Billing UI\n\nThe `BillingCard` component in `apps/app/routes/(app)/settings.tsx` handles all billing states:\n\n| State           | UI                                                             |\n| --------------- | -------------------------------------------------------------- |\n| Loading         | Muted loading text                                             |\n| Free plan       | \"You are on the Free plan\" + upgrade buttons                   |\n| Active/trialing | Plan name, status badge, renewal date, \"Manage Billing\" button |\n| Canceling       | Amber warning with access end date, portal link to restore     |\n\n## Data Fetching\n\nBilling state is fetched via a tRPC query wrapped in TanStack Query:\n\n```ts\n// apps/app/lib/queries/billing.ts\nexport function billingQueryOptions(activeOrgId?: string | null) {\n  return queryOptions({\n    queryKey: [...billingQueryKey, activeOrgId ?? null],\n    queryFn: () => trpcClient.billing.subscription.query(),\n  });\n}\n```\n\nThe query key includes `activeOrgId` so switching organizations automatically triggers a refetch. Use the `billingQueryKey` prefix for bulk invalidation after subscription changes.\n"
  },
  {
    "path": "docs/billing/index.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Billing\n\nStripe subscriptions are integrated via the [`@better-auth/stripe`](https://www.better-auth.com/docs/plugins/stripe) plugin. The auth system manages the full subscription lifecycle – customer creation, checkout, webhooks, and status tracking – so billing state lives alongside sessions and organizations in the same database.\n\nBilling is **optional** – without the `STRIPE_*` environment variables the app works normally; billing endpoints return 404 and the UI falls back to the free plan.\n\n## What's Included\n\n| Feature                                 | Implementation                              |\n| --------------------------------------- | ------------------------------------------- |\n| Three-tier plans (Free / Starter / Pro) | Config in `apps/api/lib/plans.ts`           |\n| Stripe hosted checkout                  | `auth.subscription.upgrade()` client method |\n| Customer portal (cancel, change card)   | `auth.subscription.billingPortal()`         |\n| Org-level and personal billing          | `referenceId` derived from session          |\n| Webhook-driven status sync              | Plugin-managed endpoint                     |\n| 14-day free trial on Pro                | `freeTrial: { days: 14 }` in plan config    |\n| Annual discount pricing                 | `annualDiscountPriceId` on Pro plan         |\n\n## Architecture\n\n```text\n┌─────────────┐     POST /api/auth/subscription/upgrade      ┌───────────────┐\n│   Browser   │ ──────────────────────────────────────────→  │  API Worker   │\n│    (app)    │                                              │    (Hono)     │\n│             │  ←── 302 redirect                            │               │\n│             │──→ Stripe Checkout (hosted)                  │  Better Auth  │\n│             │                                              │  + stripe()   │\n│             │    POST /api/auth/stripe/webhook             │  plugin       │\n│             │                              Stripe ────────→│  webhook ──→  │\n│             │                                              │  update DB    │\n│             │    GET  /api/trpc/billing.subscription       │               │\n│             │ ──────────────────────────────────────────→  │  tRPC router  │\n└─────────────┘  ←── subscription data (TanStack Query)      └───────────────┘\n```\n\n1. User clicks **Upgrade** – auth client calls `auth.subscription.upgrade()`\n2. Plugin creates a Stripe Checkout session – redirects browser to Stripe\n3. User completes payment – Stripe sends webhook to `/api/auth/stripe/webhook`\n4. Plugin verifies signature, updates `subscription` table\n5. Client refetches billing state via tRPC + TanStack Query\n\nMutations (upgrade, portal) go through the auth client because the plugin handles Stripe API calls, session validation, and org authorization internally. Reads go through tRPC to benefit from TanStack Query caching and org-aware cache keys.\n\n## Billing Reference\n\nBilling is tied to `session.activeOrganizationId` when present; otherwise falls back to `user.id` for personal use. One active subscription per reference ID.\n\n| Context             | `referenceId`          | Who can manage |\n| ------------------- | ---------------------- | -------------- |\n| Organization active | `activeOrganizationId` | Owner or admin |\n| No organization     | `user.id`              | The user       |\n\nThe server derives `referenceId` from the session – no client-side parameter needed. The billing query key includes `activeOrgId`, so switching organizations refetches automatically.\n\n## Plans\n\nThree tiers with enforced member limits:\n\n| Plan    | Members | Trial   | Price ID env var          |\n| ------- | ------- | ------- | ------------------------- |\n| Free    | 1       | –       | –                         |\n| Starter | 5       | –       | `STRIPE_STARTER_PRICE_ID` |\n| Pro     | 50      | 14 days | `STRIPE_PRO_PRICE_ID`     |\n\nSee [Plans & Pricing](./plans) for configuration details.\n\n## Environment Variables\n\n| Variable                     | Required    | Description                                       |\n| ---------------------------- | ----------- | ------------------------------------------------- |\n| `STRIPE_SECRET_KEY`          | For billing | Stripe secret key (`sk_test_...` / `sk_live_...`) |\n| `STRIPE_WEBHOOK_SECRET`      | For billing | Webhook signing secret (`whsec_...`)              |\n| `STRIPE_STARTER_PRICE_ID`    | For billing | Stripe price ID for Starter plan (`price_...`)    |\n| `STRIPE_PRO_PRICE_ID`        | For billing | Stripe price ID for Pro plan (`price_...`)        |\n| `STRIPE_PRO_ANNUAL_PRICE_ID` | Optional    | Annual discount price for Pro plan (`price_...`)  |\n\nSet in `.env.local` for development, Cloudflare secrets for staging/production. See [Environment Variables](/getting-started/environment-variables).\n\n## File Map\n\n| Layer  | Files                                                                                      |\n| ------ | ------------------------------------------------------------------------------------------ |\n| Schema | `db/schema/subscription.ts`, `stripeCustomerId` on user + organization tables              |\n| Server | `apps/api/lib/plans.ts`, `apps/api/lib/stripe.ts`, stripe plugin in `apps/api/lib/auth.ts` |\n| Router | `apps/api/routers/billing.ts`                                                              |\n| Client | `stripeClient` in `apps/app/lib/auth.ts`, `apps/app/lib/queries/billing.ts`                |\n| UI     | Billing card in `apps/app/routes/(app)/settings.tsx`                                       |\n"
  },
  {
    "path": "docs/billing/plans.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Plans & Pricing\n\nPlan limits are defined once in `apps/api/lib/plans.ts` and referenced by the auth plugin config (plan definitions) and the tRPC billing router (query responses).\n\n## Plan Limits\n\n```ts\n// apps/api/lib/plans.ts\nexport const planLimits = {\n  free: { members: 1 },\n  starter: { members: 5 },\n  pro: { members: 50 },\n} as const;\n```\n\nThis is the single source of truth for what each plan includes. Add new limit fields here – they'll automatically flow to both the auth plugin and tRPC responses.\n\n## Auth Plugin Configuration\n\nPlans are registered with the `@better-auth/stripe` plugin in `apps/api/lib/auth.ts`:\n\n```ts\n// apps/api/lib/auth.ts (stripe plugin config)\nstripe({\n  stripeClient: createStripeClient(env),\n  stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,\n  createCustomerOnSignUp: true,\n  subscription: {\n    enabled: true,\n    plans: [\n      {\n        name: \"starter\",\n        priceId: env.STRIPE_STARTER_PRICE_ID,\n        limits: planLimits.starter,\n      },\n      {\n        name: \"pro\",\n        priceId: env.STRIPE_PRO_PRICE_ID,\n        annualDiscountPriceId: env.STRIPE_PRO_ANNUAL_PRICE_ID,\n        limits: planLimits.pro,\n        freeTrial: { days: 14 },\n      },\n    ],\n  },\n});\n```\n\nThe free tier has no Stripe plan – users without an active subscription are treated as free. The `limits` objects are stored on the Stripe subscription metadata and returned by the plugin.\n\n## Stripe Dashboard Setup\n\nFor each paid plan, create a **Product** and **Price** in the [Stripe Dashboard](https://dashboard.stripe.com/products):\n\n1. Create a product (e.g., \"Starter Plan\")\n2. Add a recurring price (e.g., $9/month)\n3. Copy the price ID (`price_...`) to the corresponding environment variable\n\n| Plan          | Environment variable         | Product example           |\n| ------------- | ---------------------------- | ------------------------- |\n| Starter       | `STRIPE_STARTER_PRICE_ID`    | \"Starter Plan\" – $9/month |\n| Pro (monthly) | `STRIPE_PRO_PRICE_ID`        | \"Pro Plan\" – $29/month    |\n| Pro (annual)  | `STRIPE_PRO_ANNUAL_PRICE_ID` | \"Pro Plan\" – $290/year    |\n\n::: info\nUse Stripe **test mode** during development. The price IDs are different between test and live modes.\n:::\n\n## How Limits Are Exposed\n\nThe `billing.subscription` tRPC procedure returns the current plan and its limits:\n\n```ts\n// apps/api/routers/billing.ts\nconst sub = await ctx.db.query.subscription.findFirst({\n  where: (s, { eq, and, inArray }) =>\n    and(\n      eq(s.referenceId, referenceId),\n      inArray(s.status, [\"active\", \"trialing\"]),\n    ),\n});\n\nreturn {\n  plan,\n  status: sub?.status ?? null,\n  limits: planLimits[plan as PlanName],\n  // ...\n};\n```\n\nWhen no active subscription exists, it defaults to the `free` plan limits. Enforce limits in your application logic – tRPC middleware for server-side checks, UI guards for client-side gating.\n\n## Adding or Modifying Plans\n\n1. **Update limits** – edit `planLimits` in `apps/api/lib/plans.ts`\n2. **Update auth config** – add/edit the plan entry in `apps/api/lib/auth.ts`\n3. **Create Stripe product** – add the product and price in the Stripe Dashboard\n4. **Set env var** – add the new `STRIPE_*_PRICE_ID` to `.env.local` and Cloudflare secrets\n5. **Update UI** – add the plan option to the billing card in `apps/app/routes/(app)/settings.tsx`\n"
  },
  {
    "path": "docs/billing/webhooks.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Webhooks\n\nThe `@better-auth/stripe` plugin registers a webhook endpoint at `POST /api/auth/stripe/webhook` and handles signature verification, event parsing, and database updates automatically.\n\n## Events Handled\n\n| Stripe Event                    | Plugin Action                                       |\n| ------------------------------- | --------------------------------------------------- |\n| `checkout.session.completed`    | Activates the subscription                          |\n| `customer.subscription.created` | Records a new subscription                          |\n| `customer.subscription.updated` | Syncs status, period dates, cancellation scheduling |\n| `customer.subscription.deleted` | Marks the subscription as canceled                  |\n\nThe plugin updates the `subscription` table in the database – no manual event handling code is needed.\n\n## Stripe Dashboard Configuration\n\nRegister the webhook endpoint in [Stripe Dashboard → Webhooks](https://dashboard.stripe.com/webhooks):\n\n| Field        | Value                                                                                                                           |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------- |\n| Endpoint URL | `https://<your-domain>/api/auth/stripe/webhook`                                                                                 |\n| Events       | `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted` |\n\nCopy the signing secret (`whsec_...`) to `STRIPE_WEBHOOK_SECRET`.\n\n## Local Development\n\nUse the [Stripe CLI](https://docs.stripe.com/stripe-cli) to forward webhook events to your local dev server:\n\n```bash\nstripe listen --forward-to localhost:5173/api/auth/stripe/webhook\n```\n\nThe CLI prints a webhook signing secret (`whsec_...`) – copy it to your `.env.local`:\n\n```ini\nSTRIPE_WEBHOOK_SECRET=whsec_...\n```\n\n::: warning\nThe local signing secret changes each time you restart `stripe listen`. Update `.env.local` and restart the dev server if webhook verification fails.\n:::\n\n## Raw Body Handling\n\nStripe webhook verification requires the raw (unparsed) request body. The plugin reads the body via `request.text()` before Hono's body parser runs, so no special middleware configuration is needed.\n\n## Production Setup\n\nStore the webhook secret as a Cloudflare Worker secret:\n\n```bash\nwrangler secret put STRIPE_WEBHOOK_SECRET\n```\n\nAfter deploying, send a test event from the Stripe Dashboard to verify the endpoint is reachable and the signature validates correctly.\n"
  },
  {
    "path": "docs/database/index.md",
    "content": "# Database\n\nThe `db/` workspace manages the data layer with [Drizzle ORM](https://orm.drizzle.team/) and [Neon PostgreSQL](https://neon.tech/). In production, [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) pools and caches connections at the edge.\n\n## Workspace Structure\n\n```bash\ndb/\n├── schema/             # Table definitions and relations\n├── migrations/         # Auto-generated SQL migrations\n├── seeds/              # Seed data scripts\n├── scripts/            # Utilities (seed runner, export)\n├── drizzle.config.ts   # Drizzle Kit configuration\n└── index.ts            # Re-exports schema + DatabaseSchema type\n```\n\nSchema files are organized by domain – one file per entity group (e.g., `user.ts` contains the user, session, identity, and verification tables). All tables are re-exported from `schema/index.ts`.\n\n## Connection Architecture\n\nThe API worker connects to Neon through Cloudflare Hyperdrive, which provides connection pooling and optional query caching at the edge.\n\nTwo Hyperdrive bindings are available:\n\n| Binding             | Cache            | Use for                                                 |\n| ------------------- | ---------------- | ------------------------------------------------------- |\n| `HYPERDRIVE_CACHED` | 60 s query cache | Read-heavy queries where slight staleness is acceptable |\n| `HYPERDRIVE_DIRECT` | None             | Writes, real-time reads, anything requiring fresh data  |\n\nBoth are exposed in [tRPC context](/api/context) as `ctx.db` (cached) and `ctx.dbDirect` (direct):\n\n```ts\n// apps/api/lib/db.ts (simplified)\nexport function createDb(hyperdrive: Hyperdrive) {\n  const client = postgres(hyperdrive.connectionString, {\n    max: 1, // single connection per Worker isolate\n    prepare: false, // required for Hyperdrive compatibility\n  });\n  return drizzle(client, { schema, casing: \"snake_case\" });\n}\n```\n\n::: info\nIn development, Wrangler's `getPlatformProxy()` emulates the Hyperdrive bindings locally, resolving them to your `DATABASE_URL`. Your code uses the same `HYPERDRIVE_CACHED` / `HYPERDRIVE_DIRECT` bindings in both environments – no conditional connection logic needed.\n:::\n\n## Commands\n\nRun from the repo root. Append `:staging` or `:prod` to target other environments.\n\n| Command            | Description                                         |\n| ------------------ | --------------------------------------------------- |\n| `bun db:generate`  | Generate migration SQL from schema changes          |\n| `bun db:migrate`   | Apply pending migrations                            |\n| `bun db:push`      | Push schema directly (skips migration files)        |\n| `bun db:studio`    | Open Drizzle Studio browser UI                      |\n| `bun db:seed`      | Run seed scripts                                    |\n| `bun db:check`     | Check for drift between schema and migrations       |\n| `bun db:export`    | Export database via pg_dump to `db/backups/`        |\n| `bun db:typecheck` | Run TypeScript type-checking on the `db/` workspace |\n\n## Environment Targeting\n\nDatabase scripts select the environment through the `ENVIRONMENT` variable (falls back to `NODE_ENV`). Each environment loads env files in priority order:\n\n```\n.env.{env}.local  →  .env.local  →  .env\n```\n\nFor example, `bun db:push:staging` loads `.env.staging.local` first. The `DATABASE_URL` variable must be a valid `postgres://` or `postgresql://` connection string.\n\nSee [Environment Variables](/getting-started/environment-variables) for full details.\n\n## Importing Schemas\n\nThe `@repo/db` package exports two entry points:\n\n```ts\nimport * as schema from \"@repo/db\"; // full schema + DatabaseSchema type\nimport { user, session } from \"@repo/db/schema\"; // individual tables\n```\n"
  },
  {
    "path": "docs/database/migrations.md",
    "content": "# Migrations\n\nDrizzle Kit generates SQL migrations by diffing your TypeScript schema against the latest snapshot. Migration files live in `db/migrations/` alongside a journal that tracks applied versions.\n\n## Workflow\n\n**1. Edit the schema** in `db/schema/`.\n\n**2. Generate a migration:**\n\n```bash\nbun db:generate\n```\n\nThis produces a numbered SQL file (e.g., `0001_add_product_table.sql`) in `db/migrations/`.\n\n**3. Review the generated SQL.** Drizzle Kit's output is generally correct, but always check for destructive operations – column drops, type changes, or data loss.\n\n**4. Apply the migration:**\n\n```bash\nbun db:migrate\n```\n\n**5. Verify in Drizzle Studio:**\n\n```bash\nbun db:studio\n```\n\n## Push vs Migrate\n\n| Command          | What it does                              | Use when                              |\n| ---------------- | ----------------------------------------- | ------------------------------------- |\n| `bun db:migrate` | Applies pending migration files in order  | Production, staging, shared databases |\n| `bun db:push`    | Syncs schema directly, no migration files | Local development, rapid prototyping  |\n\n`push` is faster during development since it skips migration file generation. Switch to `migrate` when you need reproducible, reviewable changes.\n\n## Targeting Environments\n\nAppend `:staging` or `:prod` to run against other databases:\n\n```bash\nbun db:migrate:staging\nbun db:migrate:prod\n```\n\nThese set `ENVIRONMENT` internally, which controls which `.env.{env}.local` file is loaded. Double-check the target before running migrations against production.\n\n## Drift Detection\n\nIf schema files and migration snapshots diverge (e.g., after a manual DB change or a merge conflict), run:\n\n```bash\nbun db:check\n```\n\nThis reports discrepancies between your TypeScript schema and the migration history. Resolve by either updating the schema to match or generating a new migration to cover the gap.\n\n## Tips\n\n- **Name your migrations** – `bun db:generate --name add-product-table` produces clearer filenames than auto-numbered defaults.\n- **One concern per migration** – avoid bundling unrelated schema changes. Smaller migrations are easier to review and roll back.\n- **Never edit applied migrations** – if a migration has already run in staging or production, create a new migration to correct issues.\n- **Review before applying** – `db:generate` writes SQL to disk. Read the file before running `db:migrate`.\n"
  },
  {
    "path": "docs/database/queries.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Query Patterns\n\nCommon patterns for querying the database in tRPC procedures. All examples use Drizzle ORM's relational query API and assume access to `ctx.db` from [tRPC context](/api/context).\n\n## Multi-tenant Queries\n\nEvery query that returns user data must be scoped to the current organization. The active organization ID is available on the session:\n\n```ts\nconst products = await ctx.db.query.product.findMany({\n  where: eq(product.organizationId, ctx.session.activeOrganizationId),\n});\n```\n\n::: warning\nForgetting the organization filter leaks data across tenants. Treat this as a security invariant – every table with an `organizationId` column must filter by it.\n:::\n\n## Relations\n\nDrizzle's `with` clause loads related records in a single query:\n\n```ts\nconst org = await ctx.db.query.organization.findFirst({\n  where: eq(organization.id, orgId),\n  with: {\n    members: {\n      with: { user: true },\n    },\n  },\n});\n```\n\nSelect only the columns you need to reduce payload size:\n\n```ts\nconst products = await ctx.db.query.product.findMany({\n  where: eq(product.organizationId, orgId),\n  columns: { id: true, name: true, price: true },\n  with: {\n    creator: {\n      columns: { id: true, name: true },\n    },\n  },\n});\n```\n\n## DataLoader Pattern\n\nThe API uses a [DataLoader](https://github.com/graphql/dataloader) pattern to batch lookups and prevent N+1 queries. Loaders are defined with `defineLoader` and cached per-request in `ctx.cache`:\n\n```ts\n// apps/api/lib/loaders.ts (simplified)\nexport const userById = defineLoader(\n  Symbol(\"userById\"),\n  async (ctx, ids: readonly string[]) => {\n    const users = await ctx.db\n      .select()\n      .from(user)\n      .where(inArray(user.id, [...ids]));\n    return mapByKey(users, \"id\", ids);\n  },\n);\n```\n\nUse loaders when a procedure needs to fetch the same entity type for multiple IDs:\n\n```ts\nconst creator = await userById(ctx).load(product.createdBy);\n```\n\nSee [Context & Middleware – DataLoaders](/api/context#dataloaders) for the full pattern and how to add new loaders.\n\n## Access Control\n\nVerify organization membership before returning data:\n\n```ts\nconst membership = await ctx.db.query.member.findFirst({\n  where: and(eq(member.userId, ctx.user.id), eq(member.organizationId, orgId)),\n});\n\nif (!membership) {\n  throw new TRPCError({ code: \"FORBIDDEN\" });\n}\n```\n\nCheck roles for privileged operations:\n\n```ts\nif (membership.role !== \"owner\" && membership.role !== \"admin\") {\n  throw new TRPCError({ code: \"FORBIDDEN\" });\n}\n```\n\n## Design Patterns\n\n### Multi-tenant Data Isolation\n\nEvery domain table should reference an organization with cascade delete:\n\n```ts\nexport const yourTable = pgTable(\"your_table\", {\n  id: text()\n    .primaryKey()\n    .$defaultFn(() => generateId(\"xxx\")),\n  organizationId: text()\n    .notNull()\n    .references(() => organization.id, { onDelete: \"cascade\" }),\n  // ...\n});\n```\n\n### Soft Deletes\n\nWhen you need to preserve records for auditing:\n\n```ts\n// Schema\ndeletedAt: timestamp({ withTimezone: true, mode: \"date\" }),\n\n// Query – exclude soft-deleted records\nconst active = await ctx.db.query.product.findMany({\n  where: and(\n    eq(product.organizationId, orgId),\n    isNull(product.deletedAt),\n  ),\n});\n\n// Soft delete\nawait ctx.db\n  .update(product)\n  .set({ deletedAt: new Date() })\n  .where(eq(product.id, productId));\n```\n\n### Audit Fields\n\nTrack who created and modified records:\n\n```ts\ncreatedBy: text().references(() => user.id),\nupdatedBy: text().references(() => user.id),\n```\n\n### Batch Inserts\n\nUse array values for bulk operations:\n\n```ts\nawait ctx.db.insert(product).values([\n  { name: \"Product A\", price: 1000, organizationId: orgId },\n  { name: \"Product B\", price: 2000, organizationId: orgId },\n]);\n```\n"
  },
  {
    "path": "docs/database/schema.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Schema\n\nThe database schema lives in `db/schema/`, with one file per entity group. Drizzle ORM's `casing: \"snake_case\"` option maps camelCase TypeScript properties to snake_case database columns automatically.\n\n## Conventions\n\n**Primary keys** – All tables use application-generated prefixed CUID2 IDs (e.g., `usr_ght4k2jxm7pqbv01`). The 3-character prefix encodes the entity type for recognition in logs, URLs, and support tickets.\n\n| Model        | Prefix | Table          |\n| ------------ | ------ | -------------- |\n| user         | `usr`  | `user`         |\n| session      | `ses`  | `session`      |\n| account      | `idn`  | `identity`     |\n| verification | `vfy`  | `verification` |\n| organization | `org`  | `organization` |\n| member       | `mem`  | `member`       |\n| invitation   | `inv`  | `invitation`   |\n| passkey      | `pky`  | `passkey`      |\n| subscription | `sub`  | `subscription` |\n\nIDs are generated at the application level via `$defaultFn()` – no database sequences or UUID functions. See `db/schema/id.ts` for the implementation and [Prefixed CUID2 IDs](/specs/prefixed-ids) for design rationale.\n\n**Timestamps** – Every table has `createdAt` and `updatedAt` columns using `timestamp({ withTimezone: true, mode: \"date\" })`. `createdAt` defaults to `now()`; `updatedAt` auto-updates via `$onUpdate(() => new Date())`.\n\n**Foreign keys** – All FKs use `onDelete: \"cascade\"`. Every FK column gets a btree index named `{table}_{column}_idx`.\n\n**No enums** – `member.role` and `invitation.status` are plain `text` columns, not `pgEnum`. This avoids fragile coupling with Better Auth's role values.\n\n## Entity Relationship Diagram\n\n```mermaid\nerDiagram\n    user ||--o{ session : \"has\"\n    user ||--o{ identity : \"authenticates with\"\n    user ||--o{ passkey : \"registers\"\n    user ||--o{ member : \"belongs to\"\n    user ||--o{ invitation : \"invited by\"\n    user ||--o{ subscription : \"subscribes\"\n    organization ||--o{ member : \"has members\"\n    organization ||--o{ invitation : \"receives\"\n    organization ||--o{ subscription : \"subscribes\"\n\n    user {\n        text id PK \"usr_...\"\n        text name\n        text email UK\n        boolean email_verified\n        text image\n        boolean is_anonymous\n        text stripe_customer_id\n    }\n\n    session {\n        text id PK \"ses_...\"\n        timestamp expires_at\n        text token UK\n        text ip_address\n        text user_agent\n        text user_id FK\n        text active_organization_id\n    }\n\n    identity {\n        text id PK \"idn_...\"\n        text account_id\n        text provider_id\n        text user_id FK\n        text access_token\n        text refresh_token\n        text id_token\n        timestamp access_token_expires_at\n        timestamp refresh_token_expires_at\n        text scope\n        text password\n    }\n\n    verification {\n        text id PK \"vfy_...\"\n        text identifier\n        text value\n        timestamp expires_at\n    }\n\n    passkey {\n        text id PK \"pky_...\"\n        text name\n        text public_key\n        text credential_id UK\n        text user_id FK\n        integer counter\n        text device_type\n        boolean backed_up\n        text transports\n        text aaguid\n        timestamp last_used_at\n        text device_name\n        text platform\n    }\n\n    organization {\n        text id PK \"org_...\"\n        text name\n        text slug UK\n        text logo\n        text metadata\n        text stripe_customer_id\n    }\n\n    member {\n        text id PK \"mem_...\"\n        text user_id FK\n        text organization_id FK\n        text role\n    }\n\n    invitation {\n        text id PK \"inv_...\"\n        text email\n        text inviter_id FK\n        text organization_id FK\n        text role\n        text status\n        timestamp expires_at\n        timestamp accepted_at\n        timestamp rejected_at\n    }\n\n    subscription {\n        text id PK \"sub_...\"\n        text plan\n        text reference_id\n        text stripe_customer_id\n        text stripe_subscription_id UK\n        text status\n        timestamp period_start\n        timestamp period_end\n        timestamp trial_start\n        timestamp trial_end\n        boolean cancel_at_period_end\n        integer seats\n        text billing_interval\n    }\n```\n\n## Table Groups\n\n### Authentication Tables\n\nManaged by [Better Auth](https://www.better-auth.com/docs/concepts/database). Extend with care – changes must stay compatible with the auth framework.\n\n| Table          | File                | Purpose                                                                                                       |\n| -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |\n| `user`         | `schema/user.ts`    | User accounts – name, email, verification status, Stripe customer ID                                          |\n| `session`      | `schema/user.ts`    | Active sessions with device tracking and [active organization context](/auth/sessions)                        |\n| `identity`     | `schema/user.ts`    | OAuth credentials and email/password (Better Auth's `account` table, [renamed](/auth/#identity-table-rename)) |\n| `verification` | `schema/user.ts`    | OTP codes, email verification tokens                                                                          |\n| `passkey`      | `schema/passkey.ts` | WebAuthn credentials for [passwordless auth](/auth/passkeys)                                                  |\n\n::: warning\nAuthentication tables follow [Better Auth's schema requirements](https://www.better-auth.com/docs/concepts/database). When adding columns, register them in the auth config's `additionalFields` to ensure proper data handling.\n:::\n\n::: details user table – TypeScript definition\n\n```ts\n// db/schema/user.ts\nexport const user = pgTable(\"user\", {\n  id: text()\n    .primaryKey()\n    .$defaultFn(() => generateAuthId(\"user\")),\n  name: text().notNull(),\n  email: text().notNull().unique(),\n  emailVerified: boolean().default(false).notNull(),\n  image: text(),\n  isAnonymous: boolean().default(false).notNull(),\n  stripeCustomerId: text(),\n  createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .notNull(),\n  updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .$onUpdate(() => new Date())\n    .notNull(),\n});\n```\n\n:::\n\n### Organization Tables\n\nMulti-tenancy via Better Auth's [organization plugin](https://www.better-auth.com/docs/plugins/organization).\n\n| Table          | File                     | Purpose                                                          |\n| -------------- | ------------------------ | ---------------------------------------------------------------- |\n| `organization` | `schema/organization.ts` | Tenants / workspaces – name, slug, logo, metadata                |\n| `member`       | `schema/organization.ts` | User ↔ organization membership with roles (owner, admin, member) |\n| `invitation`   | `schema/invitation.ts`   | Pending org invitations with status lifecycle                    |\n\nKey constraints:\n\n- `member(userId, organizationId)` is unique – one membership per user per org\n- `invitation(organizationId, email)` is unique – one pending invite per email per org\n- `session.activeOrganizationId` has an index but no FK constraint (Better Auth design)\n- `organization.metadata` is `text`, not JSONB – Better Auth serializes it as a string\n\n### Billing Tables\n\nManaged by the [`@better-auth/stripe`](https://www.better-auth.com/docs/plugins/stripe) plugin. Do not insert or update records manually – the plugin handles the subscription lifecycle via Stripe webhooks.\n\n| Table          | File                     | Purpose                                         |\n| -------------- | ------------------------ | ----------------------------------------------- |\n| `subscription` | `schema/subscription.ts` | Stripe subscription state, plan, billing period |\n\nThe `referenceId` column is polymorphic: it points to `user.id` for personal billing or `organization.id` for org-level billing.\n\n## Extended Fields\n\nSeveral tables include columns beyond Better Auth's defaults:\n\n- **passkey:** `lastUsedAt` (security audits), `deviceName` (user-friendly label like \"MacBook Pro\"), `platform` (\"platform\" or \"cross-platform\")\n- **invitation:** `acceptedAt` / `rejectedAt` lifecycle timestamps\n\n## Adding a New Table\n\n**1. Create a schema file** in `db/schema/`:\n\n```ts\n// db/schema/product.ts\nimport { pgTable, text, integer, timestamp } from \"drizzle-orm/pg-core\";\nimport { relations } from \"drizzle-orm\";\nimport { generateId } from \"./id\";\nimport { organization } from \"./organization\";\nimport { user } from \"./user\";\n\nexport const product = pgTable(\"product\", {\n  id: text()\n    .primaryKey()\n    .$defaultFn(() => generateId(\"prd\")),\n  name: text().notNull(),\n  description: text(),\n  price: integer().notNull(),\n  organizationId: text()\n    .notNull()\n    .references(() => organization.id, { onDelete: \"cascade\" }),\n  createdBy: text()\n    .notNull()\n    .references(() => user.id),\n  createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .notNull(),\n  updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n    .defaultNow()\n    .$onUpdate(() => new Date())\n    .notNull(),\n});\n\nexport const productRelations = relations(product, ({ one }) => ({\n  organization: one(organization, {\n    fields: [product.organizationId],\n    references: [organization.id],\n  }),\n  creator: one(user, {\n    fields: [product.createdBy],\n    references: [user.id],\n  }),\n}));\n```\n\n**2. Export from the barrel file:**\n\n```ts\n// db/schema/index.ts\nexport * from \"./product\"; // [!code ++]\n```\n\n**3. Generate and apply the migration:**\n\n```bash\nbun db:generate\nbun db:migrate\n```\n\nSee [Migrations](./migrations) for the full workflow.\n\n## Extending Auth Tables\n\nTo add custom columns to authentication tables, update both the Drizzle schema and the Better Auth config:\n\n```ts\n// db/schema/user.ts – add the column\nexport const user = pgTable(\"user\", {\n  // ... existing fields ...\n  phoneNumber: text(), // [!code ++]\n});\n```\n\n```ts\n// apps/api/lib/auth.ts – register with Better Auth\nbetterAuth({\n  user: {\n    additionalFields: {\n      phoneNumber: { type: \"string\", required: false }, // [!code ++]\n    },\n  },\n});\n```\n\nThen [generate and apply migrations](./migrations) as usual.\n"
  },
  {
    "path": "docs/database/seeding.md",
    "content": "# Seeding\n\nSeed scripts populate your database with test data for development. They live in `db/seeds/` and are orchestrated by `db/scripts/seed.ts`.\n\n## Running Seeds\n\n```bash\nbun db:seed              # seed development database\nbun db:seed:staging      # seed staging\nbun db:seed:prod         # seed production\n```\n\nSeeds use `onConflictDoNothing()`, so they're safe to rerun without duplicating data.\n\n## Project Structure\n\n```\ndb/\n├── seeds/\n│   └── users.ts          # Creates 10 test user accounts\n└── scripts/\n    └── seed.ts           # Entry point – connects to DB, calls seed functions\n```\n\nThe seed runner imports your Drizzle config for environment resolution, creates a single-connection client, and calls each seed function in sequence.\n\n## Writing a Custom Seed\n\n**1. Create a seed file** in `db/seeds/`:\n\n```ts\n// db/seeds/products.ts\nimport type { PostgresJsDatabase } from \"drizzle-orm/postgres-js\";\nimport type * as schema from \"../schema\";\nimport { product } from \"../schema\";\n\nexport async function seedProducts(db: PostgresJsDatabase<typeof schema>) {\n  const data = [\n    { name: \"Starter Plan Guide\", price: 0, organizationId: \"org_...\" },\n    { name: \"Pro Onboarding Kit\", price: 4900, organizationId: \"org_...\" },\n  ];\n\n  await db.insert(product).values(data).onConflictDoNothing();\n  console.log(`Seeded ${data.length} products`);\n}\n```\n\n**2. Register in the seed runner:**\n\n```ts\n// db/scripts/seed.ts\nimport { seedUsers } from \"../seeds/users\";\nimport { seedProducts } from \"../seeds/products\"; // [!code ++]\n\n// In the main function:\nawait seedUsers(db);\nawait seedProducts(db); // [!code ++]\n```\n\n## Guidelines\n\n- Use realistic but obviously fake data (`alice@example.com`, not real addresses)\n- Always include `onConflictDoNothing()` so seeds are idempotent\n- Provide variety – mix of verified/unverified users, different roles, multiple orgs\n- Keep seed datasets small but representative of real usage patterns\n- Order seed calls by dependency – users before organizations before memberships\n"
  },
  {
    "path": "docs/deployment/ci-cd.md",
    "content": "# CI/CD\n\nGitHub Actions automates building, testing, and deploying. The pipeline uses two workflows: `ci.yml` for the build and conditional deploys, and `deploy.yml` as a reusable deployment workflow.\n\n## Pipeline Overview\n\n```\nPull request → build + lint + test → deploy to preview\nPush to main  → build + test       → deploy to staging\nManual dispatch (production)        → deploy to production\n```\n\nThe `ci.yml` workflow runs a single **build** job, then conditionally triggers one of three **deploy** jobs depending on the event:\n\n| Trigger             | Condition                         | Environment |\n| ------------------- | --------------------------------- | ----------- |\n| `pull_request`      | Any PR to `main`                  | Preview     |\n| `push`              | Merge to `main`                   | Staging     |\n| `workflow_dispatch` | Manual, `environment: production` | Production  |\n\n## Build Job\n\nThe build job runs in every trigger scenario:\n\n```yaml\n# .github/workflows/ci.yml – build job (simplified)\nsteps:\n  - uses: actions/checkout@v6\n  - uses: oven-sh/setup-bun@v2\n  - run: bun install --frozen-lockfile\n\n  # Lint (PRs only – merged code was already checked)\n  - run: bun prettier --check .\n  - run: bun lint\n\n  # Validate Terraform formatting\n  - run: terraform fmt -check -recursive infra/\n\n  # Build and test\n  - run: bun email:build # Email templates (needed for types)\n  - run: bun tsc --build # Type checking\n  - run: bun run test -- --run # Vitest\n  - run: bun --filter @repo/web build\n  - run: bun --filter @repo/api build\n  - run: bun --filter @repo/app build\n\n  # Upload artifacts for deploy jobs\n  - uses: actions/upload-artifact@v6\n```\n\nConcurrency is configured so only one run per PR or branch executes at a time, cancelling in-progress runs.\n\n## Deploy Workflow\n\nThe reusable `deploy.yml` workflow is called by each deploy job with environment-specific inputs:\n\n```yaml\n# .github/workflows/ci.yml – deploy job example\ndeploy-staging:\n  needs: [build]\n  if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n  uses: ./.github/workflows/deploy.yml\n  with:\n    name: Staging\n    environment: staging\n    url: https://staging.example.com\n  secrets: inherit\n```\n\nThe deploy workflow downloads build artifacts and deploys each worker via Wrangler:\n\n```yaml\n# .github/workflows/deploy.yml (simplified)\nsteps:\n  - uses: actions/checkout@v6\n  - uses: actions/download-artifact@v6\n  - uses: oven-sh/setup-bun@v2\n  - run: bun install --frozen-lockfile\n  # Deploy each worker\n  - run: bun wrangler deploy --config apps/api/wrangler.jsonc --env ${{ inputs.environment }}\n  - run: bun wrangler deploy --config apps/app/wrangler.jsonc --env ${{ inputs.environment }}\n  - run: bun wrangler deploy --config apps/web/wrangler.jsonc --env ${{ inputs.environment }}\n```\n\n::: warning\nThe `wrangler deploy` steps in `deploy.yml` are currently commented out as TODOs. Uncomment them once your Cloudflare infrastructure is provisioned and `CLOUDFLARE_API_TOKEN` is set in GitHub secrets.\n:::\n\n## Preview Deployments\n\nPreview deploys use [pr-codename](https://github.com/kriasoft/pr-codename) to generate unique subdomains for each PR (e.g., `brave-fox.example.com`). The codename is stable across pushes to the same PR.\n\n## Required Secrets\n\nConfigure these in your GitHub repository settings under **Settings → Secrets and variables → Actions**:\n\n| Secret                 | Required | Description                               |\n| ---------------------- | -------- | ----------------------------------------- |\n| `CLOUDFLARE_API_TOKEN` | Yes      | API token with Workers deploy permissions |\n\nWorker-level secrets (`BETTER_AUTH_SECRET`, Stripe keys, etc.) are set via `wrangler secret put` – not GitHub secrets. See [Cloudflare Workers: Secrets](/deployment/cloudflare#secrets).\n\n## Additional Workflow\n\nA separate `conventional-commits.yml` workflow validates PR titles against the [Conventional Commits](https://www.conventionalcommits.org/) spec using `amannn/action-semantic-pull-request`.\n"
  },
  {
    "path": "docs/deployment/cloudflare.md",
    "content": "# Cloudflare Workers\n\nEach app has its own `wrangler.jsonc` with per-environment configuration for variables, service bindings, and Hyperdrive.\n\n## Wrangler Configuration\n\nThe **web** worker is the edge router. It receives all traffic via route patterns and forwards requests to **app** and **api** workers through service bindings:\n\n```jsonc\n// apps/web/wrangler.jsonc (simplified)\n{\n  \"name\": \"example-web\",\n  \"routes\": [{ \"pattern\": \"example.com/*\", \"zone_name\": \"example.com\" }],\n  \"services\": [\n    { \"binding\": \"APP_SERVICE\", \"service\": \"example-app\" },\n    { \"binding\": \"API_SERVICE\", \"service\": \"example-api\" },\n  ],\n  \"assets\": {\n    \"directory\": \"./dist\",\n    \"run_worker_first\": [\"/\"],\n  },\n}\n```\n\nThe **api** worker has `nodejs_compat` enabled and connects to Neon through two Hyperdrive bindings (cached and direct):\n\n```jsonc\n// apps/api/wrangler.jsonc (simplified)\n{\n  \"name\": \"example-api\",\n  \"compatibility_flags\": [\"nodejs_compat\"],\n  \"hyperdrive\": [\n    { \"binding\": \"HYPERDRIVE_CACHED\", \"id\": \"your-hyperdrive-cached-id\" },\n    { \"binding\": \"HYPERDRIVE_DIRECT\", \"id\": \"your-hyperdrive-direct-id\" },\n  ],\n}\n```\n\nThe **app** worker serves the SPA with `not_found_handling: \"single-page-application\"` so all routes resolve to `index.html`.\n\n::: info\nService bindings are non-inheritable in Wrangler – each environment (`staging`, `preview`) must declare its own `services` array with the correct worker names (e.g., `example-app-staging`).\n:::\n\nSee [Architecture: Edge](/architecture/edge) for details on the service binding model.\n\n## Environment Variables\n\nEach worker declares `vars` per environment in `wrangler.jsonc`. The API worker has the most:\n\n| Variable            | Worker   | Description                                       |\n| ------------------- | -------- | ------------------------------------------------- |\n| `ENVIRONMENT`       | all      | `development`, `preview`, `staging`, `production` |\n| `APP_NAME`          | api      | Display name used in emails                       |\n| `APP_ORIGIN`        | api      | Full origin URL (e.g., `https://example.com`)     |\n| `ALLOWED_ORIGINS`   | api, app | Comma-separated list for CORS                     |\n| `RESEND_EMAIL_FROM` | api      | Sender address for transactional emails           |\n\nSee [Environment Variables](/getting-started/environment-variables) for the complete reference.\n\n## Secrets\n\nSecrets are set per worker via the Wrangler CLI. For the API worker:\n\n```bash\n# Generate a secret for Better Auth\nopenssl rand -hex 32\n\n# Set secrets (repeat for each environment: --env staging, --env preview)\nwrangler secret put BETTER_AUTH_SECRET\nwrangler secret put GOOGLE_CLIENT_ID\nwrangler secret put GOOGLE_CLIENT_SECRET\nwrangler secret put RESEND_API_KEY\nwrangler secret put STRIPE_SECRET_KEY\nwrangler secret put STRIPE_WEBHOOK_SECRET\n```\n\n::: warning\nRun `wrangler secret put` from the workspace directory (e.g., `apps/api/`) or pass `--config apps/api/wrangler.jsonc` so secrets bind to the correct worker.\n:::\n\n## Build and Deploy\n\nBuild order matters – email templates must compile before the API worker bundles them:\n\n```bash\n# Build all workspaces in dependency order\nbun build              # email → web → api → app\n\n# Deploy each worker\nbun api:deploy\nbun app:deploy\nbun web:deploy\n\n# Or deploy to a specific environment\nbun wrangler deploy --config apps/api/wrangler.jsonc --env staging\nbun wrangler deploy --config apps/app/wrangler.jsonc --env staging\nbun wrangler deploy --config apps/web/wrangler.jsonc --env staging\n```\n\n## Custom Domain\n\n1. Add your domain to Cloudflare and update nameservers at your registrar\n2. Update `routes` in `apps/web/wrangler.jsonc` with your domain\n3. Set SSL/TLS encryption mode to **Full (strict)** in the Cloudflare dashboard\n4. Enable **Always Use HTTPS**\n\nRoutes are declared in `wrangler.jsonc` and applied automatically on deploy. Terraform manages DNS records if `cloudflare_zone_id` and `hostname` are set in your environment variables.\n\n## Infrastructure with Terraform\n\nTerraform creates worker metadata, Hyperdrive configs, and DNS records. Worker code is deployed separately via Wrangler.\n\n```bash\n# Plan changes for staging\nbun infra:staging:edge:plan\n\n# Apply changes\nbun infra:staging:edge:apply\n```\n\nEach environment has its own Terraform state in `infra/envs/{dev,preview,staging,prod}/edge/`.\n"
  },
  {
    "path": "docs/deployment/index.md",
    "content": "# Deployment\n\nReact Starter Kit deploys as three Cloudflare Workers backed by a Neon PostgreSQL database. Infrastructure is managed with Terraform.\n\n## What Gets Deployed\n\n| Component          | Target             | Description                                                                |\n| ------------------ | ------------------ | -------------------------------------------------------------------------- |\n| **Web Worker**     | Cloudflare Workers | Edge router – receives all traffic, routes to app/api via service bindings |\n| **App Worker**     | Cloudflare Workers | Serves the React SPA and static assets                                     |\n| **API Worker**     | Cloudflare Workers | Hono + tRPC server, authentication, database access                        |\n| **Database**       | Neon PostgreSQL    | Managed Postgres with Hyperdrive connection pooling                        |\n| **Infrastructure** | Terraform          | Worker metadata, Hyperdrive configs, DNS records                           |\n\nSee [Architecture Overview](/architecture/) for how these components connect.\n\n## Prerequisites\n\n- **Cloudflare account** with Workers enabled\n- **Neon account** for PostgreSQL hosting ([sign up](https://get.neon.com/HD157BR))\n- **Terraform** installed (`brew install terraform` or [download](https://developer.hashicorp.com/terraform/install))\n- **Domain** added to Cloudflare DNS (optional for initial setup)\n\n## Environments\n\n| Environment | Trigger         | URL pattern              | Purpose                                                                      |\n| ----------- | --------------- | ------------------------ | ---------------------------------------------------------------------------- |\n| Development | `bun dev`       | `localhost:5173`         | Local development                                                            |\n| Preview     | Pull request    | `{codename}.example.com` | Isolated PR testing ([pr-codename](https://github.com/kriasoft/pr-codename)) |\n| Staging     | Push to `main`  | `staging.example.com`    | Pre-production validation                                                    |\n| Production  | Manual dispatch | `example.com`            | Live environment                                                             |\n\nEach environment has its own Wrangler config, Hyperdrive bindings, and Terraform state. See [CI/CD](/deployment/ci-cd) for how deployments are triggered.\n\n## Deployment Checklist\n\n1. **Provision infrastructure** – run Terraform to create workers, Hyperdrive, and DNS records\n2. **Set secrets** – configure `BETTER_AUTH_SECRET`, Stripe keys, and other secrets via Wrangler. See [Cloudflare Workers](/deployment/cloudflare) for the full list\n3. **Run migrations** – apply schema to your production database. See [Production Database](/deployment/production-database)\n4. **Build and deploy** – push code to workers. See [CI/CD](/deployment/ci-cd) or deploy manually:\n\n```bash\nbun build            # email → web → api → app\nbun api:deploy       # Deploy API worker\nbun app:deploy       # Deploy App worker\nbun web:deploy       # Deploy Web worker\n```\n\n## Section Pages\n\n- [Cloudflare Workers](/deployment/cloudflare) – Wrangler config, secrets, build and deploy\n- [Production Database](/deployment/production-database) – Neon setup, Hyperdrive, running migrations\n- [CI/CD](/deployment/ci-cd) – GitHub Actions pipelines, preview deployments\n- [Monitoring](/deployment/monitoring) – Logs, analytics, rollbacks, troubleshooting\n"
  },
  {
    "path": "docs/deployment/monitoring.md",
    "content": "# Monitoring\n\nMonitor your Workers in production using Cloudflare's built-in tools and roll back quickly when issues arise.\n\n## Wrangler Tail\n\nStream live logs from any worker:\n\n```bash\n# Tail production API logs\nwrangler tail --config apps/api/wrangler.jsonc\n\n# Filter to specific paths\nwrangler tail --config apps/api/wrangler.jsonc --search-str=\"/api/trpc\"\n\n# Tail staging\nwrangler tail --config apps/api/wrangler.jsonc --env=staging\n```\n\nLogs include request metadata, `console.log` output, and uncaught exceptions.\n\n## Cloudflare Analytics\n\nThe Cloudflare dashboard provides per-worker metrics:\n\n- **Workers → Analytics** – request count, error rate, CPU time, duration percentiles\n- **Workers → Logs** – real-time and historical log streams\n- Set up **notification policies** for error rate spikes or latency increases\n\n## Rollback\n\nIf a deploy introduces issues, roll back to the previous version:\n\n```bash\n# List recent deployments\nwrangler deployments list --config apps/api/wrangler.jsonc\n\n# Roll back to the previous stable version\nwrangler rollback --config apps/api/wrangler.jsonc \\\n  --message=\"Reverting due to auth regression\"\n```\n\nRepeat for each affected worker (`apps/app/`, `apps/web/`).\n\n::: warning\nWrangler rollback reverts worker code but not database migrations. If a deploy included schema changes that the previous code depends on differently, you may need to deploy a fix-forward migration instead. See [Database: Migrations](/database/migrations).\n:::\n\n## Troubleshooting\n\n**Worker size limit** – Cloudflare Workers have a 10 MB compressed size limit (3 MB on the free plan). If you hit it:\n\n- Check for accidentally bundled dependencies\n- Move large assets to R2 storage\n- Ensure tree shaking is working (check for side-effect imports)\n\n**Database connection issues** – If queries fail or time out:\n\n- Verify Hyperdrive IDs in `wrangler.jsonc` match Terraform output\n- Check Neon dashboard for connection limit exhaustion\n- Confirm the database isn't in auto-suspended state (first request after suspend is slower)\n\n**Authentication problems** – If sign-in fails in production:\n\n- Verify `BETTER_AUTH_SECRET` is set (`wrangler secret list --config apps/api/wrangler.jsonc`)\n- Check `APP_ORIGIN` matches your actual domain (affects cookie domain)\n- Confirm OAuth redirect URIs include your production URL. See [Social Providers](/auth/social-providers)\n\n## Cost Overview\n\n| Service            | Free tier                            | Paid                                                                                     |\n| ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------- |\n| Cloudflare Workers | 100,000 requests/day                 | [$5/month for 10M requests](https://developers.cloudflare.com/workers/platform/pricing/) |\n| Neon PostgreSQL    | 0.5 GB storage, auto-suspend compute | [Scale-to-zero billing](https://neon.tech/pricing)                                       |\n| Hyperdrive         | Included with Workers paid plan      | –                                                                                        |\n| Resend             | 100 emails/day                       | [$20/month for 50K emails](https://resend.com/pricing)                                   |\n\nA typical growth-stage project runs around **~$45/month** (Workers $5 + Neon $19 + Resend $20). Free tiers are sufficient through early production – monitor usage in the Cloudflare and Neon dashboards as traffic grows.\n"
  },
  {
    "path": "docs/deployment/production-database.md",
    "content": "# Production Database\n\nThe production database runs on [Neon PostgreSQL](https://neon.tech/) with [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) providing connection pooling at the edge.\n\n## Neon Setup\n\n1. Create a Neon project at [console.neon.tech](https://console.neon.tech/) (or via [referral](https://get.neon.com/HD157BR))\n2. Create separate databases for staging and production (or use Neon branching)\n3. Copy the connection strings – you'll need them for Hyperdrive and migrations\n\nThe connection string format: `postgresql://user:pass@host/dbname?sslmode=require`\n\n## Hyperdrive Configuration\n\nHyperdrive is provisioned via Terraform. The module in `infra/modules/cloudflare/hyperdrive/` parses the Neon connection string and creates a Hyperdrive config with connection pooling:\n\n```bash\n# Provision Hyperdrive for staging\nbun infra:staging:edge:apply\n```\n\nThis creates two Hyperdrive bindings per environment:\n\n| Binding             | Caching             | Use for                                            |\n| ------------------- | ------------------- | -------------------------------------------------- |\n| `HYPERDRIVE_CACHED` | Disabled by default | Read-heavy queries (enable in Terraform if needed) |\n| `HYPERDRIVE_DIRECT` | None                | Writes, real-time reads                            |\n\nAfter Terraform applies, copy the Hyperdrive IDs from the output into `apps/api/wrangler.jsonc` for each environment.\n\nSee [Database: Connection Architecture](/database/#connection-architecture) for how these bindings are used in application code.\n\n## Running Migrations\n\nMigrations run directly against Neon (not through Hyperdrive). The `db/` workspace provides environment-specific commands:\n\n```bash\n# Staging\nbun db:migrate:staging\n\n# Production\nbun db:migrate:prod\n```\n\nThese commands read connection strings from `.env.staging.local` and `.env.prod.local` respectively. See [Database: Migrations](/database/migrations) for the full workflow.\n\n::: warning\nAlways review generated migration SQL before running against production. Use `bun db:generate` to preview changes, then inspect the files in `db/migrations/` before applying.\n:::\n\n## Database Performance\n\n- **Connection pooling** – Hyperdrive maintains a pool at the edge, reducing cold-start latency\n- **Indexes** – add indexes for frequently queried columns, especially foreign keys used in multi-tenant filters\n- **Monitor slow queries** – use the Neon dashboard to identify and optimize slow queries\n- **Compute auto-suspend** – Neon suspends idle compute after inactivity; first request after suspend has higher latency\n"
  },
  {
    "path": "docs/email.md",
    "content": "# Email\n\nTransactional emails are built with [React Email](https://react.email/) and delivered through [Resend](https://resend.com/). The `apps/email/` workspace owns all templates and rendering – the API imports compiled templates and sends them via the Resend SDK.\n\n## Workspace Structure\n\n```bash\napps/email/\n├── components/\n│   └── BaseTemplate.tsx       # Shared header, footer, and styling\n├── templates/\n│   ├── otp-email.tsx          # OTP codes (sign-in, verification, password reset)\n│   ├── email-verification.tsx # Link-based email verification\n│   └── password-reset.tsx     # Link-based password reset\n├── emails/                    # Preview files for dev server (sample data)\n├── utils/\n│   └── render.ts              # renderEmailToHtml() / renderEmailToText()\n├── index.ts                   # Public exports\n└── package.json\n```\n\n## Templates\n\nThree templates ship out of the box, all wrapped in `BaseTemplate` for consistent branding:\n\n| Template            | Used By                                  | Trigger                    |\n| ------------------- | ---------------------------------------- | -------------------------- |\n| `OTPEmail`          | [Email & OTP](/auth/email-otp) auth flow | `emailOTP` plugin callback |\n| `EmailVerification` | Link-based email verification            | `sendVerificationEmail()`  |\n| `PasswordReset`     | Password reset flow                      | `sendPasswordReset()`      |\n\n`OTPEmail` handles three types via a single `type` prop – `\"sign-in\"`, `\"email-verification\"`, and `\"forget-password\"` – each with different copy. Password resets include an additional security warning. The separate `PasswordReset` template uses a red button to emphasize the security-sensitive action.\n\n## Development\n\nPreview templates locally with hot reload:\n\n```bash\nbun email:dev\n```\n\nThis starts the React Email preview server at `http://localhost:3001`. Files in `emails/` provide sample data for each template – edit them to test different states.\n\n::: tip\nThe email workspace must be built before the API can import templates. The root `bun dev` handles this automatically, but if you run the API standalone, run `bun email:build` first.\n:::\n\n## Sending Emails\n\nThe API sends emails through helper functions in `apps/api/lib/email.ts`. Each helper renders a template to both HTML and plain text, then sends via Resend:\n\n```ts\n// apps/api/lib/email.ts\nimport { OTPEmail, renderEmailToHtml, renderEmailToText } from \"@repo/email\";\n\nconst component = OTPEmail({\n  otp,\n  type,\n  appName: env.APP_NAME,\n  appUrl: env.APP_ORIGIN,\n});\nconst html = await renderEmailToHtml(component);\nconst text = await renderEmailToText(component);\n\nawait sendEmail(env, {\n  to: email,\n  subject: `Your ${typeLabel} code`,\n  html,\n  text,\n});\n```\n\nAvailable sender functions:\n\n| Function                  | Purpose                                                                        |\n| ------------------------- | ------------------------------------------------------------------------------ |\n| `sendOTP()`               | OTP codes for all auth flows                                                   |\n| `sendVerificationEmail()` | Link-based email verification                                                  |\n| `sendPasswordReset()`     | Password reset links                                                           |\n| `sendEmail()`             | Low-level sender (validates recipients, requires plain text fallback for HTML) |\n\n::: warning\n`sendEmail()` throws if you provide HTML without a plain text fallback. Always render both versions using `renderEmailToHtml()` and `renderEmailToText()`.\n:::\n\n### Development Shortcut\n\nIn development, `sendOTP()` also prints the code to the terminal for convenience:\n\n```txt\nOTP code for user@example.com: 482901\n```\n\nA valid `RESEND_API_KEY` is still required – the console output supplements the email, it doesn't replace it.\n\n## Adding a Template\n\n1. Create the template in `apps/email/templates/`:\n\n```tsx\n// apps/email/templates/invitation.tsx\nimport { Button, Heading, Text } from \"@react-email/components\";\nimport { BaseTemplate } from \"../components/BaseTemplate\";\n\ninterface InvitationProps {\n  inviterName: string;\n  orgName: string;\n  acceptUrl: string;\n  appName?: string;\n  appUrl?: string;\n}\n\nexport function Invitation({\n  inviterName,\n  orgName,\n  acceptUrl,\n  appName,\n  appUrl,\n}: InvitationProps) {\n  return (\n    <BaseTemplate\n      preview={`${inviterName} invited you to ${orgName}`}\n      appName={appName}\n      appUrl={appUrl}\n    >\n      <Heading\n        style={{ fontSize: \"24px\", fontWeight: \"600\", margin: \"0 0 24px\" }}\n      >\n        You're invited\n      </Heading>\n      <Text style={{ fontSize: \"16px\", lineHeight: \"24px\" }}>\n        {inviterName} invited you to join <strong>{orgName}</strong>.\n      </Text>\n      <Button href={acceptUrl}>Accept Invitation</Button>\n    </BaseTemplate>\n  );\n}\n```\n\n2. Export it from `apps/email/index.ts`:\n\n```ts\nexport { Invitation } from \"./templates/invitation.js\";\n```\n\n3. Add a preview file in `apps/email/emails/` with sample props for the dev server.\n\n4. Create a sender function in `apps/api/lib/email.ts` following the same render-then-send pattern.\n\n## Environment Variables\n\n| Variable            | Required  | Description                                                        |\n| ------------------- | --------- | ------------------------------------------------------------------ |\n| `RESEND_API_KEY`    | For email | Resend API key (`re_...`)                                          |\n| `RESEND_EMAIL_FROM` | For email | Sender address (e.g., `noreply@example.com`)                       |\n| `APP_NAME`          | No        | Used in email subject lines and branding (defaults to `\"Example\"`) |\n| `APP_ORIGIN`        | Yes       | Used for links in email footer                                     |\n\nSet in `.env.local` for development, Cloudflare secrets for staging/production. See [Environment Variables](/getting-started/environment-variables).\n\n## File Map\n\n| Layer            | Files                                         |\n| ---------------- | --------------------------------------------- |\n| Templates        | `apps/email/templates/*.tsx`                  |\n| Shared layout    | `apps/email/components/BaseTemplate.tsx`      |\n| Rendering        | `apps/email/utils/render.ts`                  |\n| Sending          | `apps/api/lib/email.ts`                       |\n| Auth integration | `emailOTP` callback in `apps/api/lib/auth.ts` |\n"
  },
  {
    "path": "docs/frontend/forms.md",
    "content": "# Forms & Validation\n\nForms use controlled React inputs with Zod for validation. There's no form library – the patterns are simple enough that a direct approach keeps things explicit.\n\n## Basic Pattern\n\nA typical form uses `useState` for input values and a tRPC mutation for submission:\n\n```tsx\nimport { Button, Input, Label } from \"@repo/ui\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { trpcClient } from \"@/lib/trpc\";\n\nfunction CreateProjectForm() {\n  const [name, setName] = useState(\"\");\n  const queryClient = useQueryClient();\n\n  const mutation = useMutation({\n    mutationFn: (input: { name: string }) =>\n      trpcClient.project.create.mutate(input),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"project\"] });\n      setName(\"\");\n    },\n  });\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        mutation.mutate({ name });\n      }}\n    >\n      <Label htmlFor=\"name\">Project name</Label>\n      <Input\n        id=\"name\"\n        value={name}\n        onChange={(e) => setName(e.target.value)}\n        required\n      />\n      <Button type=\"submit\" disabled={mutation.isPending}>\n        Create\n      </Button>\n    </form>\n  );\n}\n```\n\n## Zod Schema Sharing\n\nZod schemas are defined on tRPC procedures and can be shared with the frontend for search param validation or client-side checks. The login route uses a Zod schema with `validateSearch` to sanitize the `returnTo` param at parse time – see [Routing > Search Params](./routing.md#search-params) for the full example.\n\n## Auth Form\n\nThe auth form (`apps/app/components/auth/auth-form.tsx`) demonstrates a multi-step form pattern. It uses a state machine with three steps:\n\n```\nmethod → email → otp\n  ↑        ↑       │\n  └────────┘       │\n           ←───────┘\n```\n\nThe `useAuthForm` hook manages transitions between steps:\n\n```tsx\nconst VALID_TRANSITIONS: Record<AuthStep, AuthStep[]> = {\n  method: [\"email\"],\n  email: [\"method\", \"otp\"],\n  otp: [\"email\"],\n};\n```\n\nEach step renders conditionally based on the current state:\n\n```tsx\nexport function AuthForm({ mode = \"login\", onSuccess, returnTo }) {\n  const { step, email, isDisabled, error /* actions */ } = useAuthForm({\n    onSuccess,\n    mode,\n  });\n\n  return (\n    <div className=\"flex flex-col gap-6 w-full\">\n      {error && (\n        <div\n          role=\"alert\"\n          className=\"rounded-md bg-destructive/10 p-3 text-sm text-destructive\"\n        >\n          {error}\n        </div>\n      )}\n\n      {step === \"method\" && <MethodSelection /* ... */ />}\n      {step === \"email\" && <EmailInput /* ... */ />}\n      {step === \"otp\" && <OtpStep /* ... */ />}\n    </div>\n  );\n}\n```\n\nKey design decisions in `useAuthForm`:\n\n- **Counter-based pending ops** – handles overlapping child operations (e.g., passkey conditional UI running alongside manual click)\n- **Success guard** (`hasSucceededRef`) – prevents concurrent auth completion from multiple methods\n- **Email normalization** – trims whitespace and lowercases before API calls\n- **Error orthogonal to steps** – errors can occur at any step and are displayed at the form level\n\n## Error Display\n\nErrors are shown as alert boxes with `role=\"alert\"` for screen reader announcements:\n\n```tsx\n{\n  error && (\n    <div\n      role=\"alert\"\n      className=\"rounded-md bg-destructive/10 p-3 text-sm text-destructive\"\n    >\n      {error}\n    </div>\n  );\n}\n```\n\nFor mutation errors, check `mutation.error`:\n\n```tsx\n{\n  mutation.error && (\n    <div role=\"alert\" className=\"text-sm text-destructive\">\n      {mutation.error.message}\n    </div>\n  );\n}\n```\n\n## Loading States\n\nCoordinate disabled state across form elements to prevent double-submission:\n\n```tsx\n// useAuthForm combines multiple sources into one flag\nconst isDisabled = isLoading || pendingOps > 0 || !!isExternallyLoading;\n```\n\nApply to all interactive elements:\n\n```tsx\n<Input disabled={isDisabled} />\n<Button type=\"submit\" disabled={isDisabled || !email.trim()}>\n  Continue\n</Button>\n```\n\nFor mutations, use `isPending` from the mutation object:\n\n```tsx\n<Button type=\"submit\" disabled={mutation.isPending}>\n  {mutation.isPending ? \"Saving...\" : \"Save\"}\n</Button>\n```\n\n## Post-Submission\n\nAfter successful form submission, the caller handles cache invalidation and navigation – not the form itself:\n\n```tsx\n// apps/app/routes/(auth)/login.tsx\nasync function handleSuccess() {\n  await revalidateSession(queryClient, router);\n  await router.navigate({ to: search.returnTo ?? \"/\" });\n}\n\n<AuthForm mode=\"login\" onSuccess={handleSuccess} returnTo={search.returnTo} />;\n```\n\nThis keeps the form reusable – `AuthForm` works in both the login page and a login dialog because the caller controls what happens after success.\n"
  },
  {
    "path": "docs/frontend/routing.md",
    "content": "# Routing\n\nThe app uses [TanStack Router](https://tanstack.com/router/latest) with file-based routing. Routes are defined as files in `apps/app/routes/` and TanStack Router generates a typed route tree automatically.\n\n## Route File Convention\n\nEach file in `routes/` becomes a route. The file path determines the URL:\n\n```bash\napps/app/routes/\n├── __root.tsx              → Root layout (wraps everything)\n├── (auth)/\n│   ├── login.tsx           → /login\n│   └── signup.tsx          → /signup\n└── (app)/\n    ├── route.tsx           → Layout for all (app) routes\n    ├── index.tsx           → / (dashboard)\n    ├── settings.tsx        → /settings\n    ├── users.tsx           → /users\n    ├── analytics.tsx       → /analytics\n    ├── reports.tsx         → /reports\n    ├── dashboard.tsx       → /dashboard (redirects to /)\n    └── about.tsx           → /about\n```\n\nParenthesized directories like `(app)` and `(auth)` are **route groups** – they create layout boundaries without affecting the URL. `/settings` is the URL, not `/(app)/settings`.\n\nThe generated route tree lives at `apps/app/lib/routeTree.gen.ts`. Don't edit it – run `bun app:dev` and TanStack Router regenerates it on file changes.\n\n## Route Groups\n\nThe two route groups serve different auth requirements:\n\n| Group    | Purpose             | Auth behavior                             |\n| -------- | ------------------- | ----------------------------------------- |\n| `(app)`  | Protected app pages | Redirects to `/login` if unauthenticated  |\n| `(auth)` | Login/signup pages  | Redirects to `/` if already authenticated |\n\n## Root Route\n\nThe root route (`__root.tsx`) creates the router context and wraps everything in an error boundary:\n\n```tsx\n// apps/app/routes/__root.tsx\nexport const Route = createRootRouteWithContext<{\n  queryClient: QueryClient;\n}>()({\n  component: Root,\n});\n\nfunction Root() {\n  return (\n    <AppErrorBoundary>\n      <Outlet />\n      {import.meta.env.DEV && <TanStackRouterDevtools />}\n    </AppErrorBoundary>\n  );\n}\n```\n\nThe `queryClient` in context is what makes `beforeLoad` guards possible – route guards can prefetch or read cached data before rendering.\n\n## Auth Guards\n\n### Protecting app routes\n\nThe `(app)/route.tsx` layout guard uses a cache-first strategy for instant navigation:\n\n```tsx\n// apps/app/routes/(app)/route.tsx\nexport const Route = createFileRoute(\"/(app)\")({\n  beforeLoad: async ({ context, location }) => {\n    // Check cache first – instant when data exists\n    let session = getCachedSession(context.queryClient);\n\n    // Fetch only if cache is empty (first visit or after sign-out)\n    if (session === undefined) {\n      session = await context.queryClient.fetchQuery(sessionQueryOptions());\n    }\n\n    // Both user and session must exist for valid auth state\n    if (!session?.user || !session?.session) {\n      throw redirect({\n        to: \"/login\",\n        search: { returnTo: location.href },\n      });\n    }\n\n    return { user: session.user, session };\n  },\n  component: AppLayout,\n});\n```\n\nThis pattern makes subsequent navigations between protected routes instant – the session is already cached from the first load.\n\n### Redirecting authenticated users\n\nLogin and signup routes redirect authenticated users away:\n\n```tsx\n// apps/app/routes/(auth)/login.tsx\nexport const Route = createFileRoute(\"/(auth)/login\")({\n  validateSearch: searchSchema,\n  beforeLoad: async ({ context, search }) => {\n    try {\n      const session = await context.queryClient.fetchQuery(\n        sessionQueryOptions(),\n      );\n      if (session?.user && session?.session) {\n        throw redirect({ to: search.returnTo ?? \"/\" });\n      }\n    } catch (error) {\n      if (isRedirect(error)) throw error;\n      // Show login form for fetch errors\n    }\n  },\n  component: LoginPage,\n});\n```\n\n## Search Params\n\nValidate and transform search params with Zod. The login route sanitizes `returnTo` to prevent open redirects:\n\n```tsx\nconst searchSchema = z.object({\n  returnTo: z\n    .string()\n    .optional()\n    .transform((val) => {\n      const safe = getSafeRedirectUrl(val);\n      return safe === \"/\" ? undefined : safe;\n    })\n    .catch(undefined),\n});\n```\n\nAccess validated search params in the component:\n\n```tsx\nfunction LoginPage() {\n  const search = Route.useSearch();\n  // search.returnTo is guaranteed safe – validated at parse time\n}\n```\n\n## Navigation\n\nUse the `<Link>` component for type-safe navigation:\n\n```tsx\nimport { Link } from \"@tanstack/react-router\";\n\n<Link to=\"/settings\">Settings</Link>\n\n// Active styling\n<Link\n  to=\"/settings\"\n  activeProps={{ className: \"font-bold text-primary\" }}\n>\n  Settings\n</Link>\n\n// With search params\n<Link to=\"/login\" search={{ returnTo: \"/settings\" }}>\n  Log in\n</Link>\n```\n\nFor programmatic navigation:\n\n```tsx\nconst router = useRouter();\nawait router.navigate({ to: \"/settings\" });\n```\n\n## Adding a New Route\n\n1. Create a route file:\n\n```tsx\n// apps/app/routes/(app)/projects.tsx\nimport { createFileRoute } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(app)/projects\")({\n  component: Projects,\n});\n\nfunction Projects() {\n  return (\n    <div className=\"p-6\">\n      <h2 className=\"text-2xl font-bold\">Projects</h2>\n    </div>\n  );\n}\n```\n\n2. The route tree regenerates automatically during `bun app:dev`. The new page is available at `/projects` and protected by the `(app)` layout guard.\n\n3. Add navigation in the sidebar or header as needed. See [State & Data Fetching](./state.md) for loading data in your new route.\n\nFor more on TanStack Router, see the [official docs](https://tanstack.com/router/latest/docs/framework/react/overview).\n"
  },
  {
    "path": "docs/frontend/state.md",
    "content": "# State & Data Fetching\n\nServer state is managed with [TanStack Query](https://tanstack.com/query/latest) through a tRPC integration. Client state uses [Jotai](https://jotai.org/) atoms when needed.\n\n## tRPC Client\n\nThe tRPC client in `apps/app/lib/trpc.ts` provides two exports:\n\n```tsx\nimport { trpcClient } from \"@/lib/trpc\"; // Raw tRPC client\nimport { api } from \"@/lib/trpc\"; // TanStack Query integration\n```\n\n- **`trpcClient`** – call procedures directly (useful in query functions, `beforeLoad`, and non-React code)\n- **`api`** – creates `queryOptions` objects for use with TanStack Query hooks\n\nThe client sends requests to `/api/trpc` with batched HTTP transport and includes credentials for cookie-based auth. A logger link is added in development.\n\n## TanStack Query Defaults\n\nThe `QueryClient` in `apps/app/lib/query.ts` is configured with sensible defaults:\n\n| Option                 | Value      | Rationale                                            |\n| ---------------------- | ---------- | ---------------------------------------------------- |\n| `staleTime`            | 2 min      | Prevents redundant API calls during typical sessions |\n| `gcTime`               | 5 min      | Balances memory with instant data on back-navigation |\n| `retry`                | 3          | Exponential backoff: 1s, 2s, 4s (capped at 30s)      |\n| `refetchOnWindowFocus` | `true`     | Keeps data current after tab switches                |\n| `refetchOnReconnect`   | `\"always\"` | Overrides `staleTime` after connectivity loss        |\n\nMutations retry once with a 1s delay.\n\n## Session Query\n\nThe session query (`apps/app/lib/queries/session.ts`) is the canonical example of a query module. It overrides global defaults where auth requires different behavior:\n\n```tsx\nexport function sessionQueryOptions() {\n  return queryOptions<SessionData | null>({\n    queryKey: [\"auth\", \"session\"],\n    queryFn: async () => {\n      const response = await auth.getSession();\n      if (response.error) throw response.error;\n      return response.data;\n    },\n    // Auth state should stay fresher than general data\n    staleTime: 30_000,\n    // Don't retry 401/403 – retrying won't help\n    retry(failureCount, error) {\n      const status = getErrorStatus(error);\n      if (status === 401 || status === 403) return false;\n      return failureCount < 3;\n    },\n  });\n}\n```\n\nReturns `null` when unauthenticated – not an error. The module also exports helpers for cache access:\n\n| Export                                   | Purpose                                                     |\n| ---------------------------------------- | ----------------------------------------------------------- |\n| `useSessionQuery()`                      | Basic hook                                                  |\n| `useSuspenseSessionQuery()`              | Suspense-enabled version                                    |\n| `getCachedSession(queryClient)`          | Sync cache read (no network)                                |\n| `isAuthenticated(queryClient)`           | Binary check – requires both `user` and `session`           |\n| `signOut(queryClient)`                   | Clears server session, sets cache to `null`, hard redirects |\n| `revalidateSession(queryClient, router)` | Removes cached query so `beforeLoad` fetches fresh          |\n\n## Billing Query\n\nThe billing query demonstrates multi-tenant key design – including `activeOrgId` in the key causes automatic refetch when the user switches organizations:\n\n```tsx\n// apps/app/lib/queries/billing.ts\nexport function billingQueryOptions(activeOrgId?: string | null) {\n  return queryOptions({\n    queryKey: [\"billing\", \"subscription\", activeOrgId ?? null],\n    queryFn: () => trpcClient.billing.subscription.query(),\n  });\n}\n```\n\nUsage in a component:\n\n```tsx\nfunction BillingCard() {\n  const { data: session } = useSessionQuery();\n  const activeOrgId = session?.session?.activeOrganizationId;\n  const { data: billing, isLoading } = useBillingQuery(activeOrgId);\n  // ...\n}\n```\n\n## Calling Procedures from Components\n\nUse the `api` proxy to create query options, then pass them to TanStack Query hooks:\n\n```tsx\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport { api } from \"@/lib/trpc\";\n\nfunction UserList() {\n  const { data: users } = useSuspenseQuery(api.user.list.queryOptions());\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\nFor mutations:\n\n```tsx\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { trpcClient } from \"@/lib/trpc\";\n\nfunction CreateUserButton() {\n  const queryClient = useQueryClient();\n\n  const mutation = useMutation({\n    mutationFn: (input: { name: string; email: string }) =>\n      trpcClient.user.create.mutate(input),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"user\"] });\n    },\n  });\n\n  return (\n    <button\n      onClick={() =>\n        mutation.mutate({ name: \"Alice\", email: \"alice@example.com\" })\n      }\n    >\n      Create User\n    </button>\n  );\n}\n```\n\n## Cache Invalidation\n\nInvalidate by query key prefix to refresh related data after mutations:\n\n```tsx\n// Invalidate all user queries\nqueryClient.invalidateQueries({ queryKey: [\"user\"] });\n\n// Invalidate all billing queries (any org)\nqueryClient.invalidateQueries({ queryKey: [\"billing\", \"subscription\"] });\n```\n\nFor session changes, use `removeQueries` instead of `invalidateQueries` – this forces `beforeLoad` guards to fetch fresh data rather than serving stale cache:\n\n```tsx\nqueryClient.removeQueries({ queryKey: [\"auth\", \"session\"] });\nawait router.invalidate();\n```\n\n## Jotai Store\n\nA global Jotai store is set up in `apps/app/lib/store.ts` for cross-component client state. It's wired into the app via `StoreProvider` but not heavily used – TanStack Query handles most state needs. Use Jotai for UI state that doesn't belong in server cache (theme preference, sidebar open/closed, local filters).\n\n```tsx\nimport { atom, useAtom } from \"jotai\";\n\nconst sidebarOpenAtom = atom(true);\n\nfunction Sidebar() {\n  const [open, setOpen] = useAtom(sidebarOpenAtom);\n  // ...\n}\n```\n\nSee [Forms & Validation](./forms.md) for mutation patterns in form submissions. For library reference, see the [TanStack Query docs](https://tanstack.com/query/latest/docs/framework/react/overview), [tRPC docs](https://trpc.io/docs/client/react), and [Jotai docs](https://jotai.org/docs/introduction).\n"
  },
  {
    "path": "docs/frontend/ui.md",
    "content": "# UI\n\nThe project uses [shadcn/ui](https://ui.shadcn.com/) (new-york style) with [Tailwind CSS v4](https://tailwindcss.com/) for styling. Components live in `packages/ui/` and are shared across all apps in the monorepo.\n\n## Component Management\n\nAdd components from the shadcn/ui registry:\n\n```bash\n# Add a single component\nbun ui:add button\n\n# Add multiple components\nbun ui:add dialog card select\n\n# Interactive mode – browse and select\nbun ui:add\n\n# List installed components\nbun ui:list\n\n# Update all installed components\nbun ui:update\n```\n\nRun `bun ui:list` to see which components are currently installed.\n\n## Component Structure\n\nComponents are stored directly in `packages/ui/components/` – one file per component:\n\n```bash\npackages/ui/\n├── components/\n│   ├── avatar.tsx\n│   ├── button.tsx\n│   ├── card.tsx\n│   ├── ...\n│   └── textarea.tsx\n├── lib/\n│   └── utils.ts          # cn() utility\n├── scripts/              # CLI tooling (add, list, update)\n├── index.ts              # Barrel export\n└── package.json\n```\n\nAll components and utilities are re-exported from the package root (`index.ts`), so importing is straightforward:\n\n```tsx\nimport { Button, Card, CardHeader, CardTitle, Input, cn } from \"@repo/ui\";\n```\n\n## Using Components\n\n```tsx\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@repo/ui\";\n\nfunction FeatureCard({ title, description, children }) {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>{title}</CardTitle>\n        <CardDescription>{description}</CardDescription>\n      </CardHeader>\n      <CardContent>{children}</CardContent>\n    </Card>\n  );\n}\n```\n\n## The `cn()` Utility\n\nUse `cn()` (from `clsx` + `tailwind-merge`) for conditional and merged class names:\n\n```tsx\nimport { Button, cn } from \"@repo/ui\";\n\n<Button\n  className={cn(\n    \"transition-colors\",\n    isActive && \"bg-primary text-primary-foreground\",\n    isDisabled && \"opacity-50 cursor-not-allowed\",\n  )}\n/>;\n```\n\n`tailwind-merge` resolves conflicts – later classes override earlier ones, so `cn(\"p-4\", \"p-6\")` produces `\"p-6\"`.\n\n## Theming\n\n### CSS Variables\n\nTheme colors are defined as CSS custom properties in `apps/app/styles/globals.css` using the [OKLCH](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) color space:\n\n```css\n:root {\n  --radius: 0.625rem;\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --primary: oklch(0.205 0 0);\n  --primary-foreground: oklch(0.985 0 0);\n  --destructive: oklch(0.577 0.245 27.325);\n  /* ... */\n}\n\n.dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  /* ... */\n}\n```\n\nThese variables are mapped to Tailwind utilities in `apps/app/tailwind.config.css` via `@theme inline`, so `bg-primary`, `text-muted-foreground`, etc. resolve to the CSS variables automatically.\n\n### Dark Mode\n\nDark mode uses a custom Tailwind variant that toggles on the `dark` class:\n\n```css\n/* apps/app/tailwind.config.css */\n@custom-variant dark (&:is(.dark *));\n```\n\n### Customizing Colors\n\nTo change the color scheme, update the CSS variables in `globals.css`. The OKLCH values are independent – changing `--primary` automatically applies everywhere that uses `bg-primary`, `text-primary`, etc.\n\n## Tailwind Content Scanning\n\nThe Tailwind config uses `@source` directives to scan both app code and the shared UI package:\n\n```css\n/* apps/app/tailwind.config.css */\n@import \"tailwindcss\";\n\n@source \"./lib/**/*.{js,ts,jsx,tsx}\";\n@source \"./routes/**/*.{js,ts,jsx,tsx}\";\n@source \"./components/**/*.{js,ts,jsx,tsx}\";\n@source \"../../packages/ui/components/**/*.{ts,tsx}\";\n@source \"../../packages/ui/lib/**/*.{ts,tsx}\";\n@source \"../../packages/ui/hooks/**/*.{ts,tsx}\";\n```\n\n## Troubleshooting\n\n### Component not found\n\nIf a component import fails, verify it's installed:\n\n```bash\nbun ui:list\nbun ui:add [component-name]\n```\n\n### Styles not applying\n\n1. Check that `globals.css` is imported in `apps/app/index.tsx`\n2. Verify the Tailwind config includes `@source` paths for the UI package\n3. Check that the component file exists in `packages/ui/components/`\n\n### TypeScript errors\n\nEnsure the workspace reference is set up:\n\n```json\n// apps/app/tsconfig.json\n{\n  \"references\": [{ \"path\": \"../../packages/ui\" }]\n}\n```\n\n## Resources\n\n- [shadcn/ui Components](https://ui.shadcn.com/docs/components)\n- [Radix UI Primitives](https://www.radix-ui.com)\n- [Tailwind CSS v4](https://tailwindcss.com)\n"
  },
  {
    "path": "docs/getting-started/environment-variables.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Environment Variables\n\n## File Conventions\n\nThe project uses [Vite's env file](https://vite.dev/guide/env-and-mode#env-files) convention:\n\n| File                    | Committed | Purpose                                               |\n| ----------------------- | --------- | ----------------------------------------------------- |\n| `.env`                  | Yes       | Shared defaults (placeholder values, no real secrets) |\n| `.env.local`            | No        | Local overrides with real credentials                 |\n| `.env.staging.local`    | No        | Staging-specific overrides                            |\n| `.env.production.local` | No        | Production-specific overrides                         |\n\n`.env.local` takes precedence over `.env`. Create it by copying `.env` and filling in real values:\n\n```bash\ncp .env .env.local\n```\n\n::: warning\nNever put real secrets in `.env` – it's committed to git. Use `.env.local` for anything sensitive.\n:::\n\n## Cloudflare Worker Bindings\n\nIn production, environment variables are set as Worker secrets or bindings – not from `.env` files. Configure them in the Cloudflare dashboard or via Wrangler:\n\n```bash\nwrangler secret put BETTER_AUTH_SECRET\n```\n\nDatabase connections use [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) bindings (`HYPERDRIVE_CACHED`, `HYPERDRIVE_DIRECT`) instead of raw connection strings. See [Deployment](/deployment/) for production setup.\n\nFor local development, Wrangler reads Hyperdrive connection strings from the `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_*` variables in `.env` / `.env.local`.\n\n## Variable Reference\n\n### Application\n\n| Variable               | Required | Description                                     |\n| ---------------------- | -------- | ----------------------------------------------- |\n| `APP_NAME`             | Yes      | Display name used in emails and passkey prompts |\n| `APP_ORIGIN`           | Yes      | Full origin URL (e.g., `http://localhost:5173`) |\n| `API_ORIGIN`           | Yes      | API server URL (e.g., `http://localhost:8787`)  |\n| `ENVIRONMENT`          | Yes      | `development`, `staging`, or `production`       |\n| `GOOGLE_CLOUD_PROJECT` | Yes      | Google Cloud project ID (exposed to frontend)   |\n\n### Database\n\n| Variable                                                          | Required | Description                                |\n| ----------------------------------------------------------------- | -------- | ------------------------------------------ |\n| `DATABASE_URL`                                                    | Yes      | PostgreSQL connection string               |\n| `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_CACHED` | Dev only | Hyperdrive cached connection for local dev |\n| `CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE_DIRECT` | Dev only | Hyperdrive direct connection for local dev |\n\n### Authentication\n\n| Variable               | Required | Description                                                                           |\n| ---------------------- | -------- | ------------------------------------------------------------------------------------- |\n| `BETTER_AUTH_SECRET`   | Yes      | Secret for signing sessions and tokens. Generate with `bunx @better-auth/cli secret`  |\n| `GOOGLE_CLIENT_ID`     | Yes      | Google OAuth client ID ([console](https://console.cloud.google.com/apis/credentials)) |\n| `GOOGLE_CLIENT_SECRET` | Yes      | Google OAuth client secret                                                            |\n\nSee [Authentication](/auth/) for provider setup details.\n\n### AI\n\n| Variable         | Required | Description                                             |\n| ---------------- | -------- | ------------------------------------------------------- |\n| `OPENAI_API_KEY` | Yes      | [OpenAI](https://platform.openai.com/) API key (AI SDK) |\n\n### Email\n\n| Variable            | Required | Description                                             |\n| ------------------- | -------- | ------------------------------------------------------- |\n| `RESEND_API_KEY`    | Yes      | [Resend](https://resend.com) API key for sending emails |\n| `RESEND_EMAIL_FROM` | Yes      | Sender address (e.g., `Your App <noreply@example.com>`) |\n\n### Billing (Optional)\n\nStripe billing is optional – the app works without these variables, but billing endpoints return 404.\n\n| Variable                     | Required | Description                                |\n| ---------------------------- | -------- | ------------------------------------------ |\n| `STRIPE_SECRET_KEY`          | No       | Stripe API secret key                      |\n| `STRIPE_WEBHOOK_SECRET`      | No       | Stripe webhook signing secret              |\n| `STRIPE_STARTER_PRICE_ID`    | No       | Stripe Price ID for the Starter plan       |\n| `STRIPE_PRO_PRICE_ID`        | No       | Stripe Price ID for the Pro plan (monthly) |\n| `STRIPE_PRO_ANNUAL_PRICE_ID` | No       | Stripe Price ID for the Pro plan (annual)  |\n\nSee [Billing](/billing/) for Stripe configuration.\n\n### Cloudflare\n\n| Variable                | Required    | Description                        |\n| ----------------------- | ----------- | ---------------------------------- |\n| `CLOUDFLARE_ACCOUNT_ID` | Deploy only | Cloudflare account ID              |\n| `CLOUDFLARE_ZONE_ID`    | Deploy only | DNS zone ID for custom domains     |\n| `CLOUDFLARE_API_TOKEN`  | Deploy only | API token for Wrangler deployments |\n\n### Analytics and Search\n\n| Variable                | Required | Description                       |\n| ----------------------- | -------- | --------------------------------- |\n| `GA_MEASUREMENT_ID`     | No       | Google Analytics 4 measurement ID |\n| `ALGOLIA_APP_ID`        | No       | Algolia application ID            |\n| `ALGOLIA_ADMIN_API_KEY` | No       | Algolia admin API key             |\n"
  },
  {
    "path": "docs/getting-started/index.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Introduction\n\nReact Starter Kit is a production-ready monorepo for building SaaS web applications. It wires together authentication, database migrations, billing, email, and edge deployment so you can skip months of boilerplate and focus on your product.\n\n## Who It's For\n\n- **Indie hackers** shipping an MVP fast\n- **Startups** that need a solid foundation without vendor lock-in\n- **Teams** building multi-tenant SaaS products\n\n## Tech Stack\n\n| Layer      | Technology                                                                                                                                                                                          |\n| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Runtime    | [Bun](https://bun.sh) 1.3+, TypeScript 5.9, ESM                                                                                                                                                     |\n| Frontend   | [React](https://react.dev) 19, [TanStack Router](https://tanstack.com/router), [TanStack Query](https://tanstack.com/query), [Jotai](https://jotai.org), [Tailwind CSS](https://tailwindcss.com) v4 |\n| UI         | [shadcn/ui](https://ui.shadcn.com) (new-york style)                                                                                                                                                 |\n| Backend    | [Hono](https://hono.dev), [tRPC](https://trpc.io) 11                                                                                                                                                |\n| Auth       | [Better Auth](https://www.better-auth.com/) – email OTP, passkeys, Google OAuth, organizations                                                                                                      |\n| Billing    | [Stripe](https://stripe.com) subscriptions via Better Auth plugin                                                                                                                                   |\n| Database   | [Neon](https://neon.tech) PostgreSQL, [Drizzle ORM](https://orm.drizzle.team)                                                                                                                       |\n| Email      | [React Email](https://react.email), [Resend](https://resend.com)                                                                                                                                    |\n| Deployment | [Cloudflare Workers](https://developers.cloudflare.com/workers/), Terraform                                                                                                                         |\n| Testing    | [Vitest](https://vitest.dev) 4, Happy DOM                                                                                                                                                           |\n\n## What's Included\n\n- **Three Cloudflare Workers** – edge router, SPA, and API server connected via service bindings\n- **Type-safe API** – tRPC procedures with Zod validation, shared types between frontend and backend\n- **Multi-tenant auth** – email OTP, social login, passkeys, organizations with roles\n- **Subscription billing** – Stripe checkout, webhooks, and plan management\n- **Database toolkit** – Drizzle ORM schemas, migrations, seeding, and Hyperdrive connection pooling\n- **Email system** – React Email templates with Resend delivery\n- **AI-ready** – pre-configured instructions for Claude Code, Cursor, and Gemini CLI\n\n## How the Docs Are Organized\n\n**[Getting Started](/getting-started/quick-start)** covers setup and orientation. **[Architecture](/architecture/)** explains the worker model and request flow. Feature sections – [Frontend](/frontend/routing), [API](/api/), [Auth](/auth/), [Database](/database/), [Billing](/billing/) – document each subsystem. **[Recipes](/recipes/new-page)** provide step-by-step guides for common tasks. **[Deployment](/deployment/)** covers shipping to production.\n\nReady to start? Head to [Quick Start](./quick-start).\n"
  },
  {
    "path": "docs/getting-started/project-structure.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Project Structure\n\nThe project is a Bun monorepo with four applications, shared packages, a database workspace, and infrastructure configuration.\n\n```bash\nmy-app/\n├── apps/\n│   ├── web/           # Edge router + Astro marketing site\n│   ├── app/           # React 19 SPA (TanStack Router)\n│   ├── api/           # Hono + tRPC API server\n│   └── email/         # React Email templates\n├── packages/\n│   ├── ui/            # shadcn/ui component library\n│   ├── core/          # Shared utilities\n│   ├── ws-protocol/   # WebSocket protocol template\n│   └── typescript-config/  # Shared tsconfig presets\n├── db/                # Drizzle ORM schemas and migrations\n├── infra/             # Terraform (Cloudflare Workers, DNS)\n├── docs/              # Documentation (VitePress)\n├── scripts/           # Build and utility scripts\n└── package.json       # Monorepo root\n```\n\n## Applications\n\n### `apps/web` – Edge Router\n\nCloudflare Worker that serves as the public-facing entry point. Routes `/api/*` to the API worker and app routes (`/login`, `/settings`, etc.) to the app worker via [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). Also serves the Astro-built marketing site for unauthenticated visitors. Uses an [auth hint cookie](/adr/001-auth-hint-cookie) to decide whether `/` shows the app or the landing page.\n\n### `apps/app` – Frontend SPA\n\nReact 19 single-page application built with Vite. Uses TanStack Router for file-based routing (`apps/app/routes/`), TanStack Query for server state, Jotai for client state, and shadcn/ui components. Deployed as a Cloudflare Worker with static assets.\n\n### `apps/api` – API Server\n\nCloudflare Worker running [Hono](https://hono.dev) for HTTP routing and [tRPC](https://trpc.io) for type-safe RPC. Handles authentication (Better Auth), database queries (Drizzle ORM via Hyperdrive), and Stripe webhooks. Has `nodejs_compat` enabled – the other workers do not.\n\n### `apps/email` – Email Templates\n\n[React Email](https://react.email) templates used for OTP codes, invitations, and transactional emails. Built before the API dev server starts so templates are available at runtime. Preview with `bun email:dev`.\n\n## Packages\n\n| Package                      | Description                                                                                                                   |\n| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |\n| `packages/ui`                | [shadcn/ui](https://ui.shadcn.com) components (new-york style) with Tailwind CSS v4. Add components with `bun ui:add <name>`. |\n| `packages/core`              | Shared utilities and constants used across apps.                                                                              |\n| `packages/ws-protocol`       | WebSocket message protocol template for real-time features.                                                                   |\n| `packages/typescript-config` | Shared `tsconfig.json` presets for consistent compiler settings.                                                              |\n\n## Database Workspace\n\nThe `db/` workspace contains Drizzle ORM table definitions (`db/schema/`), migration files (`db/migrations/`), and seed scripts (`db/seeds/`). It targets Neon PostgreSQL with Cloudflare Hyperdrive for connection pooling.\n\nSee [Database Overview](/database/) for details.\n\n## Key Configuration Files\n\n| File                    | Purpose                                               |\n| ----------------------- | ----------------------------------------------------- |\n| `infra/`                | Terraform modules for Cloudflare resources            |\n| `apps/*/wrangler.jsonc` | Cloudflare Worker configuration per app               |\n| `db/drizzle.config.ts`  | Drizzle ORM migration configuration                   |\n| `.env`                  | Shared environment defaults (committed to git)        |\n| `.env.local`            | Local secrets and overrides (git-ignored)             |\n| `tsconfig.json`         | Root TypeScript project references                    |\n| `package.json`          | Monorepo root – workspaces, scripts, dev dependencies |\n"
  },
  {
    "path": "docs/getting-started/quick-start.md",
    "content": "---\noutline: [2, 3]\n---\n\n# Quick Start\n\n::: tip TL;DR\n\n```bash\ngit clone -o seed -b main --single-branch \\\n  https://github.com/kriasoft/react-starter-kit.git my-app\ncd my-app && bun install && bun dev\n```\n\n:::\n\n## Prerequisites\n\n- **[Bun](https://bun.sh)** 1.3.0 or later\n- A **[Cloudflare](https://dash.cloudflare.com/sign-up)** account (free tier works)\n\n::: info Node.js Optional\nThis project runs entirely on Bun. You don't need Node.js unless you're integrating with Node-specific tools.\n:::\n\n## Create Your Project\n\n### Option A: GitHub Template\n\n1. Go to [github.com/kriasoft/react-starter-kit](https://github.com/kriasoft/react-starter-kit)\n2. Click **\"Use this template\"** → **\"Create a new repository\"**\n3. Clone your new repository:\n\n```bash\ngit clone https://github.com/YOUR_USERNAME/YOUR_PROJECT.git\ncd YOUR_PROJECT\nbun install\n```\n\n::: tip\nThis creates a clean repository without the template's commit history.\n:::\n\n### Option B: Git Clone\n\nClone with a custom remote name so you can pull template updates later:\n\n```bash\ngit clone -o seed -b main --single-branch \\\n  https://github.com/kriasoft/react-starter-kit.git my-app\ncd my-app\nbun install\n```\n\nAdd your own repository as `origin`:\n\n```bash\ngit remote add origin https://github.com/YOUR_USERNAME/YOUR_PROJECT.git\ngit push -u origin main\n```\n\nTo pull template updates later:\n\n```bash\ngit fetch seed\ngit merge seed/main\n```\n\n::: warning\nReview template updates carefully before merging – schema or config changes may need manual resolution.\n:::\n\n## Start the Dev Server\n\n```bash\nbun dev\n```\n\nThis starts three services concurrently:\n\n| Service | URL                     | Description               |\n| ------- | ----------------------- | ------------------------- |\n| App     | `http://localhost:5173` | React SPA with hot reload |\n| API     | `http://localhost:8787` | Hono + tRPC server        |\n| Web     | `http://localhost:4321` | Astro marketing site      |\n\nYou can also start services individually:\n\n```bash\nbun app:dev     # React app only\nbun api:dev     # API server only\nbun web:dev     # Marketing site only\nbun email:dev   # Email template preview at http://localhost:3001\n```\n\n## Explore the Stack\n\n- **App** at `http://localhost:5173` – your React app with TanStack Router\n- **API** at `http://localhost:8787` – tRPC endpoints\n- **Database GUI** – run `bun db:studio` to open Drizzle Studio\n- **Email preview** – run `bun email:dev` for template preview at `http://localhost:3001`\n\n## Make It Yours\n\n1. Update branding in `apps/app/index.html`\n2. Edit the homepage at `apps/app/routes/(app)/index.tsx`\n3. Add API procedures in `apps/api/routers/`\n4. Define data models in `db/schema/`\n\n## Development Commands\n\n```bash\nbun dev          # Start all services concurrently\nbun test         # Run tests (Vitest, single run)\nbun lint         # ESLint with cache\nbun typecheck    # TypeScript type checking (tsc --build)\nbun build        # Production build: email → web → api → app\n```\n\n::: info\nAfter modifying tRPC routes, types update automatically – no manual sync needed. After editing `db/schema/`, run `bun db:generate` then `bun db:push` to apply changes.\n:::\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\n# https://vitepress.dev/reference/default-theme-home-page\nlayout: home\n\nhero:\n  name: \"React Starter Kit\"\n  # text: \"Production-ready monorepo for building fast web apps\"\n  tagline: Skip months of setup and ship your AI-powered SaaS fast. Authentication, database migrations, edge deployment, and cutting-edge React patterns all configured with industry best practices.\n  actions:\n    - theme: brand\n      text: Getting Started\n      link: /getting-started/\n    - theme: alt\n      text: View on GitHub\n      link: https://github.com/kriasoft/react-starter-kit\n    - theme: alt\n      text: Ask ChatGPT\n      link: https://chatgpt.com/g/g-69564f0a23088191846aa4072bd9397d-react-starter-kit-assistant\n    - theme: alt\n      text: Ask Gemini\n      link: https://gemini.google.com/gem/1IXFElQ2UvvZY86iL6uZLeoC-r8mp-OB-?usp=sharing\n\nfeatures:\n  - icon: 🤖\n    title: AI-First Development\n    details: Code with AI from day one – LLM instructions, tool configurations, and project context pre-built for Claude Code, Cursor, and Gemini CLI\n  - icon: 🚀\n    title: Edge-First Architecture\n    details: Built for Cloudflare Workers with optimized performance, global distribution, and instant deployment\n  - icon: ⚛️\n    title: Modern React Stack\n    details: React 19 with Vite & Astro, TanStack Router, Jotai state management, and shadcn/ui components with Tailwind CSS v4\n  - icon: 🔐\n    title: Auth + Billing Included\n    details: Better Auth with social providers, passkeys, organizations, and Stripe subscriptions via hosted checkout\n  - icon: 🏢\n    title: Multi-Tenant Database\n    details: Neon PostgreSQL with Drizzle ORM, pre-built multi-tenant schema with organizations, migrations, and type-safe queries\n  - icon: ⚡\n    title: Ship Faster\n    details: Bun runtime for instant builds, hot reload, unified tooling, and comprehensive testing setup\n---\n"
  },
  {
    "path": "docs/public/CNAME",
    "content": "reactstarter.com\n"
  },
  {
    "path": "docs/recipes/file-uploads.md",
    "content": "# File Uploads\n\nThis recipe adds file uploads using [Cloudflare R2](https://developers.cloudflare.com/r2/) with presigned URLs. A tRPC procedure validates the request and generates a signed PUT URL, then the client uploads directly to R2 – keeping the API worker lightweight.\n\n## 1. Create the R2 bucket\n\nProvision a bucket using the existing Terraform module in `infra/modules/cloudflare/r2-bucket/`:\n\n```hcl\n# infra/stacks/<env>/main.tf\nmodule \"uploads\" {\n  source     = \"../../modules/cloudflare/r2-bucket\"\n  account_id = var.cloudflare_account_id\n  name       = \"${var.project}-uploads-${var.environment}\"\n}\n```\n\nApply the change:\n\n```bash\ncd infra/stacks/<env>\nterraform apply\n```\n\n## 2. Configure bindings and secrets\n\nBind the bucket to the API worker for serving files:\n\n```jsonc\n// apps/api/wrangler.jsonc\n{\n  \"r2_buckets\": [\n    {\n      \"binding\": \"UPLOADS\",\n      \"bucket_name\": \"rsk-uploads-production\",\n    },\n  ],\n}\n```\n\nCreate an [R2 API token](https://developers.cloudflare.com/r2/api/s3/tokens/) with **Object Read & Write** permission, then add the credentials as Worker secrets:\n\n```bash\nnpx wrangler secret put R2_ACCESS_KEY_ID\nnpx wrangler secret put R2_SECRET_ACCESS_KEY\n```\n\nAdd the binding type in `apps/api/worker.ts`:\n\n```ts\ntype CloudflareEnv = {\n  HYPERDRIVE_CACHED: Hyperdrive;\n  HYPERDRIVE_DIRECT: Hyperdrive;\n  UPLOADS: R2Bucket; // [!code ++]\n} & Env;\n```\n\nAdd the S3 API credentials to the env schema in `apps/api/lib/env.ts`:\n\n```ts\nexport const envSchema = z.object({\n  // ...existing vars\n  R2_ACCESS_KEY_ID: z.string().optional(), // [!code ++]\n  R2_SECRET_ACCESS_KEY: z.string().optional(), // [!code ++]\n  R2_ENDPOINT: z.url().optional(), // [!code ++]\n  R2_BUCKET_NAME: z.string().optional(), // [!code ++]\n});\n```\n\n::: tip\n`R2_ENDPOINT` is the S3-compatible endpoint: `https://<account-id>.r2.cloudflarestorage.com`. Find it in the R2 dashboard under **Settings > S3 API**.\n:::\n\nInstall [`aws4fetch`](https://github.com/mhart/aws4fetch) for signing presigned URLs in Workers:\n\n```bash\nbun add --filter @repo/api aws4fetch\n```\n\n## 3. Create the upload procedure\n\nAdd a router that generates presigned PUT URLs and confirms uploads:\n\n```ts\n// apps/api/routers/upload.ts\nimport { AwsClient } from \"aws4fetch\";\nimport { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nconst ALLOWED_TYPES = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/webp\",\n  \"application/pdf\",\n];\nconst MAX_SIZE = 10 * 1024 * 1024; // 10 MB\nconst URL_EXPIRY = 600; // 10 minutes\n\nexport const uploadRouter = router({\n  /** Generate a presigned PUT URL for direct client-to-R2 upload. */\n  requestUpload: protectedProcedure\n    .input(\n      z.object({\n        filename: z.string().min(1),\n        contentType: z.string().refine((t) => ALLOWED_TYPES.includes(t), {\n          message: \"Unsupported file type\",\n        }),\n        size: z.number().max(MAX_SIZE),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const {\n        R2_ACCESS_KEY_ID,\n        R2_SECRET_ACCESS_KEY,\n        R2_ENDPOINT,\n        R2_BUCKET_NAME,\n      } = ctx.env;\n\n      if (\n        !R2_ACCESS_KEY_ID ||\n        !R2_SECRET_ACCESS_KEY ||\n        !R2_ENDPOINT ||\n        !R2_BUCKET_NAME\n      ) {\n        throw new TRPCError({\n          code: \"PRECONDITION_FAILED\",\n          message: \"File uploads are not configured\",\n        });\n      }\n\n      const key = `${ctx.session.activeOrganizationId}/${crypto.randomUUID()}/${input.filename}`;\n\n      const r2 = new AwsClient({\n        accessKeyId: R2_ACCESS_KEY_ID,\n        secretAccessKey: R2_SECRET_ACCESS_KEY,\n      });\n\n      const url = new URL(`${R2_ENDPOINT}/${R2_BUCKET_NAME}/${key}`);\n      url.searchParams.set(\"X-Amz-Expires\", String(URL_EXPIRY));\n\n      const signed = await r2.sign(\n        new Request(url, {\n          method: \"PUT\",\n          headers: { \"Content-Type\": input.contentType },\n        }),\n        { aws: { signQuery: true } },\n      );\n\n      return { key, uploadUrl: signed.url };\n    }),\n\n  /** Confirm upload and return metadata. */\n  complete: protectedProcedure\n    .input(z.object({ key: z.string() }))\n    .mutation(async ({ ctx, input }) => {\n      const uploads = (ctx.env as { UPLOADS?: R2Bucket }).UPLOADS;\n      if (!uploads) {\n        throw new TRPCError({\n          code: \"PRECONDITION_FAILED\",\n          message: \"R2 binding not configured\",\n        });\n      }\n\n      const object = await uploads.head(input.key);\n      if (!object) {\n        throw new TRPCError({ code: \"NOT_FOUND\", message: \"Object not found\" });\n      }\n      return { key: input.key, size: object.size };\n    }),\n});\n```\n\nRegister it in `apps/api/lib/app.ts`:\n\n```ts\nimport { uploadRouter } from \"../routers/upload.js\";\n\nconst appRouter = router({\n  // ...existing routers\n  upload: uploadRouter, // [!code ++]\n});\n```\n\n## 4. Upload from the frontend\n\n```tsx\nimport { trpcClient } from \"@/lib/trpc\";\n\nasync function uploadFile(file: File) {\n  // 1. Get a presigned URL from the API\n  const { key, uploadUrl } = await trpcClient.upload.requestUpload.mutate({\n    filename: file.name,\n    contentType: file.type,\n    size: file.size,\n  });\n\n  // 2. Upload directly to R2\n  const res = await fetch(uploadUrl, {\n    method: \"PUT\",\n    body: file,\n    headers: { \"Content-Type\": file.type },\n  });\n\n  if (!res.ok) throw new Error(`Upload failed: ${res.status}`);\n\n  // 3. Confirm and store metadata\n  return trpcClient.upload.complete.mutate({ key });\n}\n```\n\nWire it to a file input:\n\n```tsx\nfunction FileUpload() {\n  async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    const result = await uploadFile(file);\n    console.log(\"Uploaded:\", result.key);\n  }\n\n  return <input type=\"file\" accept=\"image/*,.pdf\" onChange={handleChange} />;\n}\n```\n\n## 5. Serve files\n\nAdd a Hono route that reads from R2 via the binding:\n\n```ts\n// apps/api/routes/uploads.ts\nimport { Hono } from \"hono\";\nimport type { AppContext } from \"../lib/context.js\";\n\nconst uploads = new Hono<AppContext>();\n\nuploads.get(\"/api/uploads/:key{.+}\", async (c) => {\n  const bucket = (c.env as { UPLOADS?: R2Bucket }).UPLOADS;\n  if (!bucket) return c.json({ error: \"R2 not configured\" }, 503);\n\n  const object = await bucket.get(c.req.param(\"key\"));\n  if (!object) return c.notFound();\n\n  return new Response(object.body, {\n    headers: {\n      \"Content-Type\":\n        object.httpMetadata?.contentType ?? \"application/octet-stream\",\n      \"Cache-Control\": \"public, max-age=31536000, immutable\",\n    },\n  });\n});\n\nexport { uploads };\n```\n\nMount it in `apps/api/lib/app.ts`:\n\n```ts\nimport { uploads } from \"../routes/uploads.js\";\n\napp.route(\"/\", uploads); // [!code ++]\n```\n\nFiles are served at `/api/uploads/<key>`.\n\n## Reference\n\n- [Cloudflare R2 docs](https://developers.cloudflare.com/r2/) – bucket API, S3 compatibility, pricing\n- [R2 S3 API tokens](https://developers.cloudflare.com/r2/api/s3/tokens/) – creating API credentials\n- [aws4fetch](https://github.com/mhart/aws4fetch) – lightweight AWS Signature V4 for Workers\n- [Security Checklist](/security/checklist) – file upload validation (type, size, content)\n- [Add a tRPC Procedure](/recipes/new-procedure) – procedure patterns\n"
  },
  {
    "path": "docs/recipes/new-page.md",
    "content": "# Add a Page\n\nThis recipe walks through adding a new route to the app. All routes live in `apps/app/routes/` and are auto-discovered by [TanStack Router](https://tanstack.com/router/latest).\n\n## 1. Create the route file\n\nAdd a file under the `(app)` layout group so it inherits the auth guard and shell layout:\n\n```\napps/app/routes/(app)/projects.tsx\n```\n\n```tsx\nimport { createFileRoute } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/(app)/projects\")({\n  component: Projects,\n});\n\nfunction Projects() {\n  return (\n    <div className=\"p-6\">\n      <h1 className=\"text-2xl font-bold mb-4\">Projects</h1>\n      <p className=\"text-muted-foreground\">Your projects will appear here.</p>\n    </div>\n  );\n}\n```\n\nRun `bun app:dev` – TanStack Router regenerates `lib/routeTree.gen.ts` automatically and the page is available at `/projects`.\n\n## 2. Add navigation\n\nOpen the sidebar or header component and add a link:\n\n```tsx\nimport { Link } from \"@tanstack/react-router\";\n\n<Link to=\"/projects\" className=\"...\">\n  Projects\n</Link>;\n```\n\n`<Link>` is type-safe – TypeScript will error if the route doesn't exist.\n\n## 3. Fetch data\n\nUse a tRPC query hook inside the component:\n\n```tsx\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport { trpcClient } from \"@/lib/trpc\";\nimport { queryOptions } from \"@tanstack/react-query\";\n\nfunction projectsQueryOptions() {\n  return queryOptions({\n    queryKey: [\"projects\"],\n    queryFn: () => trpcClient.project.list.query(),\n  });\n}\n\nfunction Projects() {\n  const { data } = useSuspenseQuery(projectsQueryOptions());\n\n  return (\n    <div className=\"p-6\">\n      <h1 className=\"text-2xl font-bold mb-4\">Projects</h1>\n      <ul>\n        {data.projects.map((p) => (\n          <li key={p.id}>{p.name}</li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\nSee [State & Data Fetching](/frontend/state) for more patterns.\n\n## 4. Add search params (optional)\n\nValidate query string parameters with Zod:\n\n```tsx\nimport { z } from \"zod\";\n\nconst searchSchema = z.object({\n  page: z.number().default(1),\n  q: z.string().optional(),\n});\n\nexport const Route = createFileRoute(\"/(app)/projects\")({\n  validateSearch: searchSchema,\n  component: Projects,\n});\n\nfunction Projects() {\n  const { page, q } = Route.useSearch();\n  // ...\n}\n```\n\n## 5. Add a public page (optional)\n\nTo create a page that doesn't require authentication, place it under the `(auth)` layout group or directly in `routes/`:\n\n```\napps/app/routes/(auth)/pricing.tsx\n```\n\nPages outside `(app)/` skip the auth guard and don't render the app shell layout.\n\n## Reference\n\n- [Routing](/frontend/routing) – file conventions, layouts, and route guards\n- [TanStack Router docs](https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing)\n"
  },
  {
    "path": "docs/recipes/new-procedure.md",
    "content": "# Add a tRPC Procedure\n\nThis recipe adds a new tRPC procedure with input validation and wires it up from the API to the frontend.\n\n## 1. Create the router file\n\nAdd a new router in `apps/api/routers/`:\n\n```ts\n// apps/api/routers/project.ts\nimport { z } from \"zod\";\nimport { schema } from \"@repo/db\";\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const projectRouter = router({\n  list: protectedProcedure.query(async ({ ctx }) => {\n    const projects = await ctx.db.query.project.findMany({\n      where: (p, { eq }) =>\n        eq(p.organizationId, ctx.session.activeOrganizationId!),\n      orderBy: (p, { desc }) => desc(p.createdAt),\n    });\n    return { projects };\n  }),\n\n  create: protectedProcedure\n    .input(\n      z.object({\n        name: z.string().min(1).max(100),\n        description: z.string().max(500).optional(),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const [project] = await ctx.db\n        .insert(schema.project)\n        .values({\n          ...input,\n          organizationId: ctx.session.activeOrganizationId!,\n        })\n        .returning();\n      return project;\n    }),\n});\n```\n\nUse `protectedProcedure` for authenticated endpoints and `publicProcedure` for unauthenticated ones. Protected procedures guarantee `ctx.session` and `ctx.user` are non-null.\n\n## 2. Register the router\n\nImport and add it to the app router in `apps/api/lib/app.ts`:\n\n```ts\nimport { projectRouter } from \"../routers/project.js\";\n\nconst appRouter = router({\n  billing: billingRouter,\n  user: userRouter,\n  organization: organizationRouter,\n  project: projectRouter, // [!code ++]\n});\n```\n\nThe procedure is now callable at `/api/trpc/project.list` and `/api/trpc/project.create`.\n\n## 3. Call from the frontend\n\nCreate a query options helper in `apps/app/lib/queries/`:\n\n```ts\n// apps/app/lib/queries/project.ts\nimport {\n  queryOptions,\n  useQuery,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport { trpcClient } from \"../trpc\";\n\nexport function projectListOptions() {\n  return queryOptions({\n    queryKey: [\"projects\"],\n    queryFn: () => trpcClient.project.list.query(),\n  });\n}\n\nexport function useProjectList() {\n  return useQuery(projectListOptions());\n}\n```\n\nUse in a component:\n\n```tsx\nimport { useProjectList } from \"@/lib/queries/project\";\n\nfunction ProjectList() {\n  const { data, isLoading } = useProjectList();\n\n  if (isLoading) return <p>Loading...</p>;\n\n  return (\n    <ul>\n      {data?.projects.map((p) => (\n        <li key={p.id}>{p.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n## 4. Call a mutation\n\n```tsx\nimport { trpcClient } from \"@/lib/trpc\";\nimport { useQueryClient } from \"@tanstack/react-query\";\n\nfunction CreateProjectButton() {\n  const queryClient = useQueryClient();\n\n  async function handleCreate() {\n    await trpcClient.project.create.mutate({\n      name: \"New Project\",\n    });\n    // Invalidate the list so it refetches\n    await queryClient.invalidateQueries({ queryKey: [\"projects\"] });\n  }\n\n  return <button onClick={handleCreate}>Create Project</button>;\n}\n```\n\n## Reference\n\n- [Procedures](/api/procedures) – query vs mutation, public vs protected\n- [Validation & Errors](/api/validation-errors) – Zod input schemas and error handling\n- [State & Data Fetching](/frontend/state) – TanStack Query patterns\n"
  },
  {
    "path": "docs/recipes/new-table.md",
    "content": "# Add a Database Table\n\nThis recipe walks through adding a new database table, from schema definition to querying it in the API.\n\n## 1. Define the schema\n\nCreate a file in `db/schema/` with your table definition:\n\n```ts\n// db/schema/project.ts\nimport { relations } from \"drizzle-orm\";\nimport { index, pgTable, text, timestamp } from \"drizzle-orm/pg-core\";\nimport { generateId } from \"./id\";\nimport { organization } from \"./organization\";\n\nexport const project = pgTable(\n  \"project\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateId(\"prj\")),\n    name: text().notNull(),\n    description: text(),\n    organizationId: text()\n      .notNull()\n      .references(() => organization.id, { onDelete: \"cascade\" }),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [index(\"project_organization_id_idx\").on(table.organizationId)],\n);\n\nexport const projectRelations = relations(project, ({ one }) => ({\n  organization: one(organization, {\n    fields: [project.organizationId],\n    references: [organization.id],\n  }),\n}));\n\nexport type Project = typeof project.$inferSelect;\nexport type NewProject = typeof project.$inferInsert;\n```\n\nKey conventions:\n\n- **IDs** – use `generateId(\"xxx\")` with a unique 3-letter prefix (see [Schema](/database/schema) for existing prefixes)\n- **Timestamps** – always include `createdAt` and `updatedAt` with timezone\n- **Foreign keys** – use `onDelete: \"cascade\"` for owned resources\n- **Indexes** – add indexes on columns used in `WHERE` or `JOIN` clauses\n- **Casing** – write TypeScript in camelCase; Drizzle converts to snake_case automatically\n\n## 2. Export from the barrel file\n\n```ts\n// db/schema/index.ts\nexport * from \"./project\"; // [!code ++]\n```\n\n## 3. Generate and apply the migration\n\n```bash\nbun db:generate    # Creates a new SQL migration file in db/migrations/\nbun db:push        # Applies it to your local database\n```\n\nReview the generated SQL in `db/migrations/` before applying to staging or production.\n\n## 4. Add seed data (optional)\n\nCreate a seed function:\n\n```ts\n// db/seeds/projects.ts\nimport type { PostgresJsDatabase } from \"drizzle-orm/postgres-js\";\nimport type * as schema from \"../schema\";\nimport { project } from \"../schema\";\n\nexport async function seedProjects(db: PostgresJsDatabase<typeof schema>) {\n  const projects = [\n    { name: \"Acme Dashboard\", organizationId: \"org_...\" },\n    { name: \"Mobile App\", organizationId: \"org_...\" },\n  ];\n\n  for (const p of projects) {\n    await db.insert(project).values(p).onConflictDoNothing();\n  }\n\n  console.log(`Seeded ${projects.length} projects`);\n}\n```\n\nCall it from `db/scripts/seed.ts`:\n\n```ts\nimport { seedProjects } from \"../seeds/projects\";\n\nawait seedProjects(db);\n```\n\n## 5. Query in a tRPC procedure\n\n```ts\n// apps/api/routers/project.ts\nimport { protectedProcedure, router } from \"../lib/trpc.js\";\n\nexport const projectRouter = router({\n  list: protectedProcedure.query(async ({ ctx }) => {\n    return ctx.db.query.project.findMany({\n      where: (p, { eq }) =>\n        eq(p.organizationId, ctx.session.activeOrganizationId!),\n      orderBy: (p, { desc }) => desc(p.createdAt),\n    });\n  }),\n});\n```\n\nSee [Add a tRPC Procedure](/recipes/new-procedure) for the full frontend wiring.\n\n## Reference\n\n- [Schema](/database/schema) – column conventions, ID prefixes, entity reference\n- [Migrations](/database/migrations) – migration workflow and best practices\n- [Query Patterns](/database/queries) – multi-tenant queries, joins, transactions\n"
  },
  {
    "path": "docs/recipes/teams.md",
    "content": "# Add Teams\n\nTeams let you create subgroups within organizations. This recipe enables Better Auth's [teams feature](https://www.better-auth.com/docs/plugins/organization#teams) and wires it into the existing schema.\n\n## 1. Add the schema\n\nCreate `db/schema/team.ts`:\n\n```typescript\nimport { relations } from \"drizzle-orm\";\nimport { index, pgTable, text, timestamp, unique } from \"drizzle-orm/pg-core\";\nimport { generateId } from \"./id\";\nimport { organization } from \"./organization\";\nimport { user } from \"./user\";\n\nexport const team = pgTable(\n  \"team\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateId(\"tea\")),\n    name: text().notNull(),\n    organizationId: text()\n      .notNull()\n      .references(() => organization.id, { onDelete: \"cascade\" }),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [index(\"team_organization_id_idx\").on(table.organizationId)],\n);\n\nexport const teamMember = pgTable(\n  \"team_member\",\n  {\n    id: text()\n      .primaryKey()\n      .$defaultFn(() => generateId(\"tmb\")),\n    teamId: text()\n      .notNull()\n      .references(() => team.id, { onDelete: \"cascade\" }),\n    userId: text()\n      .notNull()\n      .references(() => user.id, { onDelete: \"cascade\" }),\n    createdAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .notNull(),\n    updatedAt: timestamp({ withTimezone: true, mode: \"date\" })\n      .defaultNow()\n      .$onUpdate(() => new Date())\n      .notNull(),\n  },\n  (table) => [\n    unique(\"team_member_team_user_unique\").on(table.teamId, table.userId),\n    index(\"team_member_team_id_idx\").on(table.teamId),\n    index(\"team_member_user_id_idx\").on(table.userId),\n  ],\n);\n\nexport const teamRelations = relations(team, ({ one, many }) => ({\n  organization: one(organization, {\n    fields: [team.organizationId],\n    references: [organization.id],\n  }),\n  members: many(teamMember),\n}));\n\nexport const teamMemberRelations = relations(teamMember, ({ one }) => ({\n  team: one(team, {\n    fields: [teamMember.teamId],\n    references: [team.id],\n  }),\n  user: one(user, {\n    fields: [teamMember.userId],\n    references: [user.id],\n  }),\n}));\n```\n\nExport it from `db/schema/index.ts`:\n\n```typescript\nexport * from \"./team\"; // [!code ++]\n```\n\n## 2. Extend session and invitation tables\n\nAdd `activeTeamId` to the session table in `db/schema/user.ts`:\n\n```typescript\nexport const session = pgTable(\n  \"session\",\n  {\n    // ...existing columns\n    activeOrganizationId: text(),\n    activeTeamId: text(), // [!code ++]\n  },\n  // ...\n);\n```\n\nAdd `teamId` to the invitation table in `db/schema/invitation.ts` for team-scoped invitations:\n\n```typescript\nexport const invitation = pgTable(\n  \"invitation\",\n  {\n    // ...existing columns\n    teamId: text().references(() => team.id, { onDelete: \"cascade\" }), // [!code ++]\n  },\n  // ...\n);\n```\n\n## 3. Enable the teams plugin\n\nIn `apps/api/lib/auth.ts`, add the new tables to the Drizzle adapter schema and enable teams in the organization plugin:\n\n```typescript\ndatabase: drizzleAdapter(db, {\n  provider: \"pg\",\n  schema: {\n    // ...existing mappings\n    team: Db.team, // [!code ++]\n    teamMember: Db.teamMember, // [!code ++]\n  },\n}),\n\n// ...\n\nplugins: [\n  organization({\n    allowUserToCreateOrganization: true,\n    organizationLimit: 5,\n    creatorRole: \"owner\",\n    teams: { enabled: true }, // [!code ++]\n  }),\n],\n```\n\nIn `apps/app/lib/auth.ts`, enable teams on the client:\n\n```typescript\nexport const auth = createAuthClient({\n  // ...\n  plugins: [\n    organizationClient({\n      teams: { enabled: true }, // [!code ++]\n    }),\n    // ...other plugins\n  ],\n});\n```\n\n## 4. Apply the migration\n\n```bash\nbun db:generate\nbun db:push\n```\n\n## 5. Use the teams API\n\nCreate a team within the active organization:\n\n```ts\nawait auth.organization.createTeam({\n  name: \"Engineering\",\n});\n```\n\nSet the active team for the current session:\n\n```ts\nawait auth.organization.setActiveTeam({\n  teamId: \"tea_...\",\n});\n```\n\nList teams and manage members:\n\n```ts\n// List teams in the active organization\nconst { data: teams } = await auth.organization.listTeams();\n\n// Add a member to a team\nawait auth.organization.addTeamMember({\n  teamId: \"tea_...\",\n  userId: \"usr_...\",\n});\n\n// Remove a member from a team\nawait auth.organization.removeTeamMember({\n  teamId: \"tea_...\",\n  userId: \"usr_...\",\n});\n```\n\nThe active team ID is available in the session as `session.activeTeamId`, alongside the existing `session.activeOrganizationId`.\n\n## Reference\n\n- [Better Auth organization plugin – Teams](https://www.better-auth.com/docs/plugins/organization#teams)\n- [Organizations & Roles](/auth/organizations) – base organization setup\n"
  },
  {
    "path": "docs/recipes/websockets.md",
    "content": "# WebSockets\n\nThis recipe adds real-time WebSocket communication using the `@repo/ws-protocol` package and [WS-Kit](https://github.com/kriasoft/ws-kit).\n\n## 1. Define a message\n\nAdd a new message schema in `packages/ws-protocol/messages.ts`:\n\n```ts\n// packages/ws-protocol/messages.ts\nimport { message, z } from \"@ws-kit/zod\";\n\nexport const ChatMessage = message(\"CHAT_MESSAGE\", {\n  channelId: z.string(),\n  text: z.string().min(1).max(2000),\n  sentAt: z.number(),\n});\n```\n\nMessages follow the envelope structure `{ type, meta, payload }` and are validated with Zod at runtime. For request/response patterns, use `rpc()` instead:\n\n```ts\nimport { rpc, z } from \"@ws-kit/zod\";\n\nexport const GetMessages = rpc(\n  \"GET_MESSAGES\",\n  { channelId: z.string(), limit: z.number().default(50) },\n  \"MESSAGES\",\n  { messages: z.array(z.object({ id: z.string(), text: z.string() })) },\n);\n```\n\n## 2. Add a handler\n\nRegister the message handler in `packages/ws-protocol/router.ts`:\n\n```ts\n// packages/ws-protocol/router.ts\nimport { ChatMessage } from \"./messages\";\n\nexport function createAppRouter(): Router<AppData> {\n  const router = createRouter<AppData>()\n    .plugin(withZod())\n    // ... existing handlers\n    .on(ChatMessage, (ctx) => {\n      // Broadcast to all clients subscribed to this channel\n      ctx.publish(`channel:${ctx.payload.channelId}`, ChatMessage, {\n        channelId: ctx.payload.channelId,\n        text: ctx.payload.text,\n        sentAt: ctx.payload.sentAt,\n      });\n    });\n\n  return router;\n}\n```\n\nPublishing requires the pub/sub plugin – see step 3.\n\n## 3. Start the server\n\nCreate a WebSocket server entry point using Bun's native WebSocket support:\n\n```ts\nimport { createBunHandler } from \"@ws-kit/bun\";\nimport { memoryPubSub } from \"@ws-kit/memory\";\nimport { withPubSub } from \"@ws-kit/pubsub\";\nimport { createAppRouter } from \"@repo/ws-protocol/router\";\n\nconst router = createAppRouter().plugin(\n  withPubSub({ adapter: memoryPubSub() }),\n);\n\nconst { fetch: handleWebSocket, websocket } = createBunHandler(router, {\n  authenticate(req) {\n    // Validate auth token, return initial connection data\n    return { connectedAt: Date.now() };\n  },\n});\n\nBun.serve({\n  port: 3001,\n  fetch(req, server) {\n    if (new URL(req.url).pathname === \"/ws\") {\n      return handleWebSocket(req, server);\n    }\n    return new Response(\"WebSocket server\");\n  },\n  websocket,\n});\n```\n\nFor Cloudflare Workers, use `@ws-kit/cloudflare` with Durable Objects instead of `@ws-kit/bun`.\n\n## 4. Connect from the frontend\n\n```ts\nimport { Ping, Pong, ChatMessage } from \"@repo/ws-protocol\";\n\nconst ws = new WebSocket(\"ws://localhost:3001/ws\");\n\n// Listen for messages\nws.addEventListener(\"message\", (event) => {\n  const msg = JSON.parse(event.data);\n  if (msg.type === ChatMessage.type) {\n    console.log(\"Chat:\", msg.payload.text);\n  }\n});\n\n// Send a message\nws.send(\n  JSON.stringify({\n    type: \"CHAT_MESSAGE\",\n    meta: {},\n    payload: {\n      channelId: \"general\",\n      text: \"Hello!\",\n      sentAt: Date.now(),\n    },\n  }),\n);\n```\n\nFor a type-safe client with automatic reconnection, use `@ws-kit/client`:\n\n```ts\nimport { wsClient } from \"@ws-kit/client/zod\";\nimport { ChatMessage, Pong } from \"@repo/ws-protocol\";\n\nconst client = wsClient({\n  url: \"ws://localhost:3001/ws\",\n  reconnect: { enabled: true },\n});\n\nclient.on(ChatMessage, (msg) => {\n  console.log(\"Chat:\", msg.payload.text);\n});\n\nawait client.connect();\nclient.send(ChatMessage, {\n  channelId: \"general\",\n  text: \"Hello!\",\n  sentAt: Date.now(),\n});\n```\n\n## 5. Run the example\n\nThe `packages/ws-protocol/` workspace includes a working example server:\n\n```bash\nbun --filter @repo/ws-protocol example\n```\n\nConnect with any WebSocket client (e.g., `wscat -c ws://localhost:3000/ws`) and send:\n\n```json\n{\"type\": \"PING\", \"meta\": {}, \"payload\": {}}\n{\"type\": \"ECHO\", \"meta\": {}, \"payload\": {\"text\": \"Hello\"}}\n```\n\n## Reference\n\n- [WS-Kit documentation](https://github.com/kriasoft/ws-kit) – message schemas, router API, pub/sub\n- [Architecture Overview](/architecture/) – worker boundaries and service bindings\n- [Add a tRPC Procedure](/recipes/new-procedure) – for HTTP-based API endpoints\n"
  },
  {
    "path": "docs/security/checklist.md",
    "content": "# Security Best Practices Checklist\n\nA comprehensive security checklist for React Starter Kit applications. Review this checklist during development, before deployment, and regularly in production.\n\n## Development Phase\n\n### Code Security\n\n#### Input Validation\n\n- [ ] Validate all user inputs on both client and server\n- [ ] Use Zod schemas for type-safe validation\n- [ ] Sanitize HTML content to prevent XSS\n- [ ] Validate file uploads (type, size, content)\n- [ ] Implement rate limiting on forms and APIs\n\n```typescript\n// Example: tRPC input validation with Zod\nexport const userRouter = router({\n  create: publicProcedure\n    .input(\n      z.object({\n        email: z.string().email(),\n        name: z.string().min(1).max(100),\n        age: z.number().int().positive().max(120),\n      }),\n    )\n    .mutation(async ({ input }) => {\n      // Input is already validated\n    }),\n});\n```\n\n#### Authentication & Authorization\n\n- [ ] Use Better Auth for authentication\n- [ ] Implement proper session management\n- [ ] Use secure session storage (httpOnly cookies)\n- [ ] Implement CSRF protection\n- [ ] Check permissions on every protected route\n- [ ] Log authentication events\n\n```typescript\n// Example: Protected tRPC procedure\nexport const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {\n  if (!ctx.session?.user) {\n    throw new TRPCError({ code: \"UNAUTHORIZED\" });\n  }\n  return next({ ctx: { ...ctx, user: ctx.session.user } });\n});\n```\n\n#### Data Protection\n\n- [ ] Never log sensitive data (passwords, tokens, PII)\n- [ ] Use parameterized queries (Drizzle ORM)\n- [ ] Encrypt sensitive data at rest\n- [ ] Implement proper error handling without data leaks\n- [ ] Use HTTPS for all communications\n- [ ] Validate and sanitize database queries\n\n```typescript\n// Example: Safe database query with Drizzle\nconst users = await db\n  .select()\n  .from(usersTable)\n  .where(eq(usersTable.email, email)); // Parameterized, prevents SQL injection\n```\n\n### Secret Management\n\n#### Environment Variables\n\n- [ ] Store secrets in `.env.local` (never commit)\n- [ ] Use `.env` only for non-sensitive defaults\n- [ ] Document required variables in `.env`\n- [ ] Validate environment variables at startup\n- [ ] Use different secrets for each environment\n\n```typescript\n// Example: Environment validation\nconst env = z\n  .object({\n    DATABASE_URL: z.string().url(),\n    JWT_SECRET: z.string().min(32),\n    SMTP_PASSWORD: z.string(),\n    PUBLIC_API_URL: z.string().url(), // Safe for client\n  })\n  .parse(process.env);\n```\n\n#### Production Secrets\n\n- [ ] Use Cloudflare Workers secrets for production\n- [ ] Rotate secrets regularly\n- [ ] Never hardcode secrets in code\n- [ ] Audit secret access logs\n- [ ] Use secret scanning in CI/CD\n\n### Dependencies\n\n#### Package Management\n\n- [ ] Run `bun audit` regularly\n- [ ] Review new dependencies before adding\n- [ ] Check dependency licenses\n- [ ] Enable Dependabot alerts\n- [ ] Keep dependencies up to date\n- [ ] Use lock files (`bun.lockb`)\n\n```bash\n# Security audit commands\nbun audit                    # Check for vulnerabilities\nbun update --latest          # Update dependencies\nbun pm ls                    # List all dependencies\n```\n\n#### Supply Chain Security\n\n- [ ] Verify package authenticity\n- [ ] Use specific versions (not wildcards)\n- [ ] Review dependency source code for critical packages\n- [ ] Monitor for dependency hijacking\n- [ ] Use SubResource Integrity (SRI) for CDN resources\n\n## Pre-Deployment Phase\n\n### Security Headers\n\n#### Configure Headers\n\n- [ ] Content Security Policy (CSP)\n- [ ] X-Frame-Options\n- [ ] X-Content-Type-Options\n- [ ] Strict-Transport-Security (HSTS)\n- [ ] Referrer-Policy\n- [ ] Permissions-Policy\n\n```typescript\n// Example: Security headers in Hono\napp.use(\"*\", async (c, next) => {\n  await next();\n  c.header(\"X-Frame-Options\", \"DENY\");\n  c.header(\"X-Content-Type-Options\", \"nosniff\");\n  c.header(\"Strict-Transport-Security\", \"max-age=31536000\");\n  c.header(\"Content-Security-Policy\", \"default-src 'self'\");\n});\n```\n\n### API Security\n\n#### tRPC Security\n\n- [ ] Validate all inputs with Zod\n- [ ] Implement rate limiting\n- [ ] Use proper error codes\n- [ ] Don't expose internal errors\n- [ ] Log suspicious activities\n- [ ] Implement request timeouts\n\n```typescript\n// Example: Rate limiting middleware\nconst rateLimiter = new Map();\n\nexport const rateLimit = middleware(async ({ ctx, next }) => {\n  const key = ctx.ip;\n  const limit = rateLimiter.get(key) || 0;\n\n  if (limit > 10) {\n    throw new TRPCError({ code: \"TOO_MANY_REQUESTS\" });\n  }\n\n  rateLimiter.set(key, limit + 1);\n  setTimeout(() => rateLimiter.delete(key), 60000);\n\n  return next();\n});\n```\n\n#### CORS Configuration\n\n- [ ] Configure allowed origins explicitly\n- [ ] Don't use wildcard (\\*) in production\n- [ ] Validate Origin header\n- [ ] Configure allowed methods and headers\n- [ ] Use credentials carefully\n\n### Client Security\n\n#### React Security\n\n- [ ] Avoid dangerouslySetInnerHTML\n- [ ] Sanitize user-generated content\n- [ ] Use Content Security Policy\n- [ ] Validate URLs before navigation\n- [ ] Implement proper error boundaries\n- [ ] Don't expose sensitive data in state\n\n```typescript\n// Example: Safe HTML rendering\nimport DOMPurify from 'isomorphic-dompurify'\n\nfunction SafeHTML({ content }: { content: string }) {\n  const sanitized = DOMPurify.sanitize(content)\n  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />\n}\n```\n\n#### Browser Storage\n\n- [ ] Don't store sensitive data in localStorage\n- [ ] Use httpOnly cookies for sessions\n- [ ] Encrypt sensitive client-side data\n- [ ] Clear storage on logout\n- [ ] Implement storage quotas\n\n## Deployment Phase\n\n### Infrastructure Security\n\n#### Cloudflare Workers\n\n- [ ] Configure WAF rules\n- [ ] Enable DDoS protection\n- [ ] Set up rate limiting\n- [ ] Configure security headers\n- [ ] Enable bot protection\n- [ ] Monitor security events\n\n```toml\n# Example: wrangler.toml security config\n[env.production]\ncompatibility_date = \"2024-01-01\"\ncompatibility_flags = [\"nodejs_compat\"]\n\n[env.production.rate_limiting]\nenabled = true\nrequests_per_minute = 60\n```\n\n#### CI/CD Security\n\n- [ ] Use least privilege for CI/CD tokens\n- [ ] Store secrets securely (GitHub Secrets)\n- [ ] Enable branch protection\n- [ ] Require code reviews\n- [ ] Run security checks in pipeline\n- [ ] Sign commits and releases\n\n```yaml\n# Example: GitHub Actions security\n- name: Run security audit\n  run: |\n    bun audit\n    bun test:security\n\n- name: SAST Scan\n  uses: github/super-linter@v5\n  env:\n    VALIDATE_JAVASCRIPT_ES: true\n    VALIDATE_TYPESCRIPT_ES: true\n```\n\n### Monitoring & Logging\n\n#### Security Monitoring\n\n- [ ] Log authentication attempts\n- [ ] Monitor for suspicious patterns\n- [ ] Set up security alerts\n- [ ] Track rate limit violations\n- [ ] Monitor dependency vulnerabilities\n- [ ] Review logs regularly\n\n```typescript\n// Example: Security event logging\nfunction logSecurityEvent(event: {\n  type: string;\n  user?: string;\n  ip: string;\n  details: Record<string, any>;\n}) {\n  console.log(\n    JSON.stringify({\n      timestamp: new Date().toISOString(),\n      severity: \"SECURITY\",\n      ...event,\n    }),\n  );\n}\n```\n\n#### Incident Response\n\n- [ ] Have incident response plan ready\n- [ ] Configure security notifications\n- [ ] Set up backup and recovery\n- [ ] Document security contacts\n- [ ] Test incident procedures\n- [ ] Keep security playbook updated\n\n## Production Phase\n\n### Ongoing Security\n\n#### Regular Tasks\n\n- [ ] Weekly: Review security alerts\n- [ ] Monthly: Run dependency audits\n- [ ] Quarterly: Security assessment\n- [ ] Annually: Penetration testing\n- [ ] Ongoing: Security training\n\n#### Security Updates\n\n- [ ] Monitor security advisories\n- [ ] Apply patches promptly\n- [ ] Test updates in staging\n- [ ] Document security changes\n- [ ] Communicate with users about security\n\n### Compliance\n\n#### Data Protection\n\n- [ ] Implement GDPR compliance (if applicable)\n- [ ] Add privacy policy\n- [ ] Implement data deletion\n- [ ] Log data access\n- [ ] Encrypt personal data\n\n#### Security Documentation\n\n- [ ] Maintain SECURITY.md\n- [ ] Document security procedures\n- [ ] Keep incident log\n- [ ] Update security checklist\n- [ ] Train team on security\n\n## Quick Security Wins\n\nFor immediate security improvements:\n\n1. **Run Security Audit**\n\n   ```bash\n   bun audit\n   ```\n\n2. **Add Security Headers**\n\n   ```typescript\n   // apps/api/src/index.ts\n   app.use(securityHeaders());\n   ```\n\n3. **Implement Rate Limiting**\n\n   ```typescript\n   // apps/api/src/middleware.ts\n   app.use(rateLimit({ limit: 100, window: \"1m\" }));\n   ```\n\n4. **Enable HTTPS Redirect**\n\n   ```typescript\n   // apps/web/src/index.ts\n   if (location.protocol === \"http:\") {\n     location.replace(\"https:\" + window.location.href.substring(5));\n   }\n   ```\n\n5. **Add Input Validation**\n\n   ```typescript\n   // Use Zod everywhere\n   const schema = z.object({\n     /* ... */\n   });\n   const validated = schema.parse(input);\n   ```\n\n## Security Resources\n\n### Tools\n\n- [OWASP ZAP](https://www.zaproxy.org/) – Security scanning\n- [Snyk](https://snyk.io/) – Dependency scanning\n- [GitHub Security](https://github.com/security) – Security features\n- [Mozilla Observatory](https://observatory.mozilla.org/) – Security assessment\n\n### Documentation\n\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [React Security](https://react.dev/learn/security)\n- [Cloudflare Security](https://developers.cloudflare.com/workers/platform/security/)\n- [Better Auth Docs](https://better-auth.com/docs/security)\n\n### Emergency Contacts\n\n- Security Issues: `security@kriasoft.com`\n- GitHub Security: [Security Advisories](https://github.com/kriasoft/react-starter-kit/security)\n- CVE Database: [MITRE CVE](https://cve.mitre.org/)\n\n---\n\n_Review this checklist regularly and update based on new threats and best practices._\n"
  },
  {
    "path": "docs/security/incident-playbook.md",
    "content": "# Security Incident Response Playbook\n\nThis playbook provides step-by-step procedures for handling security incidents in React Starter Kit projects. Each procedure includes specific actions, tools, and decision criteria.\n\n## Quick Reference\n\n- **Security Email**: `security@kriasoft.com`\n- **Incident Tracking**: GitHub Security Advisories\n- **Communication Channel**: Email (encrypted when possible)\n- **Escalation**: Project maintainers via GitHub\n\n## Incident Classification\n\n### Determining Severity\n\nUse this decision tree to classify incidents:\n\n```\nIs remote code execution possible?\n├─ Yes → CRITICAL (P0)\n└─ No → Can authentication be bypassed?\n    ├─ Yes → CRITICAL (P0)\n    └─ No → Is sensitive data exposed?\n        ├─ Yes (all users) → CRITICAL (P0)\n        ├─ Yes (subset) → HIGH (P1)\n        └─ No → Is privilege escalation possible?\n            ├─ Yes → HIGH (P1)\n            └─ No → Is XSS present?\n                ├─ Yes (auth flow) → HIGH (P1)\n                ├─ Yes (other) → MEDIUM (P2)\n                └─ No → Is CSRF possible?\n                    ├─ Yes → MEDIUM (P2)\n                    └─ No → LOW (P3)\n```\n\n## Phase 1: Initial Response\n\n### Step 1.1: Acknowledge Report (0-2 hours)\n\n**Actions:**\n\n1. Send acknowledgment email with template:\n\n   ```\n   Subject: [RSK-SEC-YYYY-NNN] Security Report Received\n\n   Thank you for your security report. We have received your submission\n   and assigned tracking ID: RSK-SEC-YYYY-NNN\n\n   We will begin our initial assessment and respond within [TIMEFRAME].\n\n   Please keep this vulnerability confidential while we investigate.\n   ```\n\n2. Create private GitHub issue for tracking\n3. Assign initial responder\n\n**Tools:** Email client, GitHub Issues (private)\n\n### Step 1.2: Initial Assessment (2-24 hours)\n\n**Actions:**\n\n1. Review report for completeness\n2. Attempt to reproduce vulnerability\n3. Determine affected components\n4. Classify severity using decision tree\n\n**Decision Points:**\n\n- If cannot reproduce – Request clarification\n- If critical – Immediately notify maintainers\n- If valid – Proceed to Phase 2\n\n### Step 1.3: Form Response Team\n\n**For Critical/High severity:**\n\n- Lead: Project maintainer\n- Developer: Fix implementation\n- Reviewer: Code review and testing\n- Communicator: External updates\n\n**For Medium/Low severity:**\n\n- Lead: Available maintainer\n- Developer: Fix implementation\n\n## Phase 2: Investigation & Containment\n\n### Step 2.1: Deep Dive Analysis (Day 1-2)\n\n**Actions:**\n\n1. Set up isolated test environment\n2. Reproduce vulnerability with minimal test case\n3. Identify root cause\n4. Document attack vectors\n5. Check for similar vulnerabilities\n\n**Checklist:**\n\n- [ ] Vulnerability reproduced\n- [ ] Root cause identified\n- [ ] Attack surface mapped\n- [ ] Similar code patterns checked\n- [ ] Impact assessment complete\n\n### Step 2.2: Temporary Mitigation (If Critical)\n\n**Actions:**\n\n1. Develop temporary workaround\n2. Test workaround doesn't break functionality\n3. Document workaround for users\n4. Publish security bulletin with mitigation\n\n**Template for Security Bulletin:**\n\n```markdown\n## Security Bulletin: [TITLE]\n\n**Date**: [DATE]\n**Severity**: [CRITICAL/HIGH]\n**Status**: Under Investigation\n\n### Summary\n\nWe are investigating a security vulnerability in React Starter Kit.\n\n### Temporary Mitigation\n\nUntil a patch is available, users should:\n\n1. [Specific mitigation steps]\n2. [Additional steps]\n\n### Timeline\n\n- Patch expected: [DATE]\n- Full disclosure: After patch\n\n### Contact\n\nReport issues to: `security@kriasoft.com`\n```\n\n## Phase 3: Development & Testing\n\n### Step 3.1: Develop Fix (Varies by severity)\n\n**Actions:**\n\n1. Create private branch for fix\n2. Implement minimal fix (no refactoring)\n3. Add regression tests\n4. Document code changes\n\n**Code Review Checklist:**\n\n- [ ] Fix addresses root cause\n- [ ] No new vulnerabilities introduced\n- [ ] Tests cover vulnerability scenario\n- [ ] Changes are minimal and focused\n- [ ] No sensitive info in comments/commits\n\n### Step 3.2: Testing Protocol\n\n**Test Environments:**\n\n1. Local development\n2. Isolated staging\n3. Integration testing\n4. Performance impact\n\n**Test Cases:**\n\n- [ ] Original PoC no longer works\n- [ ] Legitimate functionality preserved\n- [ ] No performance regression\n- [ ] No new error conditions\n- [ ] Edge cases handled\n\n### Step 3.3: Prepare Release\n\n**Actions:**\n\n1. Update version numbers\n2. Write release notes\n3. Prepare security advisory\n4. Request CVE (if applicable)\n\n**CVE Request Template:**\n\n```\n[Contact GitHub Security for CVE]\nRepository: react-starter-kit\nVulnerability Type: [TYPE]\nAffected Versions: < X.Y.Z\nFixed Version: X.Y.Z\nDescription: [DESCRIPTION]\n```\n\n## Phase 4: Release & Disclosure\n\n### Step 4.1: Coordinated Release\n\n**Release Checklist:**\n\n- [ ] Code merged to main branch\n- [ ] Version tagged and released\n- [ ] Security advisory drafted\n- [ ] Reporter notified of release date\n- [ ] Release notes prepared\n\n### Step 4.2: Public Disclosure\n\n**Actions:**\n\n1. Publish GitHub Security Advisory\n2. Update SECURITY.md if needed\n3. Send notification to users (if critical)\n4. Credit reporter\n\n**Security Advisory Template:**\n\n```markdown\n## [CVE-YYYY-NNNNN] [Vulnerability Title]\n\n**Severity**: [Critical/High/Medium/Low]\n**Affected Versions**: < X.Y.Z\n**Patched Version**: X.Y.Z\n\n### Description\n\n[Clear description of vulnerability]\n\n### Impact\n\n[Potential impact on users]\n\n### Patches\n\nUpdate to version X.Y.Z or later.\n\n### Workarounds\n\n[If any temporary workarounds exist]\n\n### References\n\n- [Links to fixes]\n- [Links to documentation]\n\n### Credit\n\nReported by [Name] ([Organization])\n```\n\n### Step 4.3: User Communication\n\n**For Critical vulnerabilities:**\n\n1. Email registered users (if applicable)\n2. Post on project blog/website\n3. Social media announcement\n4. Update documentation\n\n**Communication Template:**\n\n```\nSubject: [ACTION REQUIRED] Security Update for React Starter Kit\n\nA critical security vulnerability has been discovered and patched.\n\nAction Required:\n1. Update to version X.Y.Z immediately\n2. Review security advisory: [LINK]\n3. Apply any additional mitigations\n\nDetails: [BRIEF DESCRIPTION]\n\nQuestions: `security@kriasoft.com`\n```\n\n## Phase 5: Post-Incident Review\n\n### Step 5.1: Incident Retrospective (Within 1 week)\n\n**Meeting Agenda:**\n\n1. Timeline review\n2. What went well\n3. What could improve\n4. Action items\n5. Policy updates needed\n\n**Questions to Answer:**\n\n- How was the vulnerability introduced?\n- Why wasn't it caught earlier?\n- How can we prevent similar issues?\n- Was our response adequate?\n- What tools/processes need improvement?\n\n### Step 5.2: Implement Improvements\n\n**Common Improvements:**\n\n- Add security linting rules\n- Enhance test coverage\n- Update coding guidelines\n- Improve dependency management\n- Add security checkpoints to CI/CD\n\n### Step 5.3: Documentation Updates\n\n**Update as needed:**\n\n- This playbook\n- SECURITY.md\n- Development guidelines\n- CI/CD configurations\n- Security checklist\n\n## Appendix A: Tools & Resources\n\n### Security Tools\n\n- **Dependency Scanning**: `bun audit`, Dependabot\n- **Static Analysis**: ESLint security plugins\n- **Secret Scanning**: GitHub secret scanning, truffleHog\n- **SAST**: Semgrep, CodeQL\n- **Testing**: Vitest for security tests\n\n### Communication Tools\n\n- **Encrypted Email**: PGP/GPG\n- **Secure File Transfer**: Age encryption\n- **Private Issues**: GitHub Security Advisories\n\n### External Resources\n\n- [GitHub Security Advisories](https://docs.github.com/en/code-security/security-advisories)\n- [CVE Request Process](https://cve.mitre.org/cve/request_id.html)\n- [OWASP Incident Response](https://owasp.org/www-project-incident-response)\n- [NIST Incident Handling Guide](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-61r2.pdf)\n\n## Appendix B: Contact Templates\n\n### Reporter Follow-up\n\n```\nSubject: Re: [RSK-SEC-YYYY-NNN] Status Update\n\nThank you for your patience. Here's an update on your report:\n\nStatus: [In Progress/Testing Fix/Ready for Release]\nSeverity: [Confirmed as X]\nTimeline: [Expected resolution date]\n\n[Any questions for reporter]\n\nWe'll notify you before public disclosure.\n```\n\n### Maintainer Escalation\n\n```\nSubject: [URGENT] Critical Security Issue - Immediate Action Required\n\nA critical vulnerability has been reported:\n\nTracking: RSK-SEC-YYYY-NNN\nType: [Vulnerability type]\nImpact: [Brief impact description]\nStatus: [Confirmed/Under Investigation]\n\nRequired Actions:\n1. [Immediate actions needed]\n2. [Review assignments]\n\nDetails in private issue: [Link]\n```\n\n### Release Notification\n\n```\nSubject: Security Release Scheduled - [DATE]\n\nSecurity release details:\n\nVersion: X.Y.Z\nRelease Date: [DATE TIME UTC]\nSeverity: [Level]\nCVE: [If assigned]\n\nPre-release checklist:\n- [ ] Code reviewed and tested\n- [ ] Advisory prepared\n- [ ] Reporter notified\n- [ ] Release notes ready\n\nPlease confirm readiness by [DATE].\n```\n\n## Revision History\n\n- v1.0.0 - Initial playbook creation\n- Updates logged in commit history\n\n---\n\n_This playbook is a living document. Update it based on lessons learned from each incident._\n"
  },
  {
    "path": "docs/security/policy-template.md",
    "content": "# Security Policy & Incident Response Plan\n\n## Our Security Commitment\n\nThe [PROJECT_NAME] team takes security seriously. We appreciate responsible disclosure of vulnerabilities and are committed to working with security researchers to keep our project secure.\n\nThis document outlines our security policy, incident response procedures, and how to report vulnerabilities.\n\n## Scope\n\nThis security policy applies to vulnerabilities discovered within the `[REPOSITORY_NAME]` repository. The scope includes:\n\n- [List specific components, modules, or features]\n- [Example: Core application code and configurations]\n- [Example: API endpoints and authentication systems]\n- [Example: Database schemas and data handling]\n- [Example: Build and deployment processes]\n\n### Out of Scope\n\nThe following are considered **out of scope** for this policy:\n\n- Vulnerabilities in third-party dependencies already publicly disclosed\n- Issues requiring physical access or compromised credentials\n- Social engineering attacks\n- [Add project-specific exclusions]\n\n## Supported Versions\n\nWe provide security updates for the following versions:\n\n| Version | Supported          |\n| ------- | ------------------ |\n| [X.Y.Z] | :white_check_mark: |\n| [X.Y-1] | :x:                |\n\n## Incident Response\n\n- **Report Security Issues**: `[SECURITY_EMAIL]`\n- **Initial Response**: Within [RESPONSE_TIME]\n- **Critical Issues**: Escalated immediately to maintainers\n\n## Reporting a Vulnerability\n\n**⚠️ DO NOT report security vulnerabilities through public GitHub issues.**\n\nReport to: **`[SECURITY_EMAIL]`**\n\n### Include in Your Report\n\n1. **Description**: Clear explanation of the vulnerability and impact\n2. **Steps to Reproduce**: Minimal steps to demonstrate the issue\n3. **Proof of Concept**: Code or screenshots if applicable\n4. **Affected Version**: Branch or commit hash\n5. **Suggested Fix**: Optional recommendations\n\n## Incident Response Process\n\n### Severity Classification\n\n| Level             | Description                   | Examples                                                  |\n| ----------------- | ----------------------------- | --------------------------------------------------------- |\n| **Critical (P0)** | Immediate threat to all users | Remote code execution, authentication bypass, data breach |\n| **High (P1)**     | Significant security impact   | Privilege escalation, data exposure, XSS in auth flows    |\n| **Medium (P2)**   | Limited security impact       | XSS in non-critical areas, CSRF vulnerabilities           |\n| **Low (P3)**      | Minor security issues         | Information disclosure, security misconfigurations        |\n\n### Response Timeline\n\n| Severity | Initial Response | Fix Target  | Disclosure   |\n| -------- | ---------------- | ----------- | ------------ |\n| Critical | 2 days           | 14 days     | Upon patch   |\n| High     | 3 days           | 30 days     | Upon patch   |\n| Medium   | 5 days           | 60 days     | Upon patch   |\n| Low      | 7 days           | Best effort | With release |\n\n### Incident Response Phases\n\n#### Phase 1: Detection & Analysis\n\n- Acknowledge receipt and assign tracking ID\n- Validate and reproduce vulnerability\n- Assess severity and impact\n- Notify team if critical\n\n#### Phase 2: Containment\n\n- Implement temporary mitigations\n- Document affected components\n- Begin fix development\n- Prepare communication plan\n\n#### Phase 3: Remediation\n\n- Develop and test permanent fix\n- Prepare security patch\n- Request CVE if appropriate\n- Coordinate disclosure timeline\n\n#### Phase 4: Recovery & Disclosure\n\n- Release patched version\n- Publish security advisory\n- Update documentation\n- Credit reporter\n\n#### Phase 5: Post-Incident Review\n\n- Document lessons learned\n- Update security practices\n- Improve detection\n- Update policies\n\n## Communication Expectations\n\n- All communications via email\n- Regular updates throughout process\n- Clear explanation if not a vulnerability\n- Confidentiality until patched\n\n## Safe Harbor\n\nWe consider security research conducted in good faith to be:\n\n- Authorized under applicable laws\n- Exempt from Terms of Service restrictions\n- Protected from legal action\n\nRequirements for safe harbor:\n\n- Follow this policy\n- Report responsibly\n- Avoid privacy violations\n- No exploitation beyond demonstration\n\n## Recognition\n\nWe value security researchers' contributions:\n\n- Public credit in advisories (unless anonymous)\n- Security acknowledgments\n- Letter of appreciation upon request\n\n## Security Best Practices for Users\n\n### Essential Security Measures\n\n1. **Secret Management**\n   - Never commit secrets to version control\n   - Use environment variables for sensitive data\n   - Implement secret rotation\n   - Enable secret scanning\n\n2. **Authentication & Authorization**\n   - Implement proper session management\n   - Use strong password policies\n   - Enable multi-factor authentication\n   - Regular auth system updates\n\n3. **Dependencies**\n   - Regular security audits\n   - Keep dependencies updated\n   - Review licenses and advisories\n   - Use dependency scanning tools\n\n4. **Deployment**\n   - HTTPS everywhere\n   - Proper CORS policies\n   - Security headers (CSP, HSTS, etc.)\n   - Regular security assessments\n\n5. **Code Security**\n   - Input validation\n   - Output encoding\n   - Parameterized queries\n   - Principle of least privilege\n\n## Additional Resources\n\n- [Security Advisories]([GITHUB_SECURITY_URL])\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Project Documentation]([DOCS_URL])\n- [Security Checklist]([CHECKLIST_URL])\n\n---\n\n---\n\n_Template Instructions: Replace all [BRACKETS] with project-specific information and adjust timelines to match your team's capacity._\n\nThank you for helping us keep [PROJECT_NAME] secure!\n"
  },
  {
    "path": "docs/specs/auth-form.md",
    "content": "# Auth Flow UX Specification\n\nTarget UX inspired by Linear's authentication flow.\n\n## Design Principles\n\n1. **Progressive disclosure** – Show only what's needed at each step\n2. **Method selection first** – Let users choose their auth method before showing inputs\n3. **Minimal friction** – Reduce cognitive load with focused, single-purpose views\n4. **Clear navigation** – Easy to go back and switch methods\n\n## Flow Structure\n\n### Login (`/login`)\n\n```text\nStep 1: Method Selection\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│    Log in to [App Name]     │\n│                             │\n│  ┌───────────────────────┐  │\n│  │ Continue with Google  │  │\n│  └───────────────────────┘  │\n│  ┌───────────────────────┐  │\n│  │ Continue with email   │  │\n│  └───────────────────────┘  │\n│  ┌───────────────────────┐  │\n│  │ Log in with passkey   │  │\n│  └───────────────────────┘  │\n│                             │\n│  Don't have an account?     │\n│  Sign up                    │\n└─────────────────────────────┘\n\nStep 2: Email Input (after clicking \"Continue with email\")\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│  What's your email address? │\n│                             │\n│  ┌───────────────────────┐  │\n│  │ Enter your email...   │  │\n│  └───────────────────────┘  │\n│  ┌───────────────────────┐  │\n│  │ Continue with email   │  │\n│  └───────────────────────┘  │\n│                             │\n│  ← Back to login            │\n└─────────────────────────────┘\n\nStep 3: OTP Verification\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│  Check your email           │\n│  We sent a code to          │\n│  user@example.com           │\n│                             │\n│  ┌─┬─┬─┬─┬─┬─┐              │\n│  │ │ │ │ │ │ │  (6 digits)  │\n│  └─┴─┴─┴─┴─┴─┘              │\n│                             │\n│  Resend code                │\n│  ← Back                     │\n└─────────────────────────────┘\n```\n\n### Signup (`/signup`)\n\n```text\nStep 1: Method Selection\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│    Create your account      │\n│                             │\n│  ┌───────────────────────┐  │\n│  │ Continue with Google  │  │\n│  └───────────────────────┘  │\n│  ┌───────────────────────┐  │\n│  │ Continue with email   │  │\n│  └───────────────────────┘  │\n│                             │\n│  By signing up, you agree   │\n│  to our Terms and Privacy   │\n│  Policy.                    │\n│                             │\n│  Already have an account?   │\n│  Log in                     │\n└─────────────────────────────┘\n\nStep 2: Email Input (after clicking \"Continue with email\")\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│  What's your email address? │\n│                             │\n│  ┌───────────────────────┐  │\n│  │ Enter your email...   │  │\n│  └───────────────────────┘  │\n│  ┌───────────────────────┐  │\n│  │ Continue with email   │  │\n│  └───────────────────────┘  │\n│                             │\n│  By signing up, you agree   │\n│  to our Terms and Privacy   │\n│  Policy.                    │\n│                             │\n│  ← Back to sign up          │\n└─────────────────────────────┘\n\nStep 3: OTP Verification\n┌─────────────────────────────┐\n│         [Logo]              │\n│                             │\n│  Check your email           │\n│  We sent a code to          │\n│  user@example.com           │\n│                             │\n│  ┌─┬─┬─┬─┬─┬─┐              │\n│  │ │ │ │ │ │ │  (6 digits)  │\n│  └─┴─┴─┴─┴─┴─┘              │\n│                             │\n│  Resend code                │\n│  ← Back to email            │\n└─────────────────────────────┘\n```\n\nNote: No passkey option on signup (passkeys require existing account).\n\n## Third-Party Auth Behavior\n\n- **Google**: On failure or user cancel, return to method selection with inline error.\n- **Passkey**: On failure (not supported, no credential, user cancel), return to method selection with inline error and a short hint to use email instead.\n- **Network/system errors**: Show a non-blocking toast and keep the user on the current step.\n\n## Key Differences from Current Implementation\n\n| Aspect       | Current                           | Target                                    |\n| ------------ | --------------------------------- | ----------------------------------------- |\n| Initial view | All methods + email input visible | Method selection buttons only             |\n| Email input  | Always visible with divider       | Separate step after clicking email button |\n| Layout       | Card with optional right panel    | Centered content, no card                 |\n| Headings     | \"Welcome\" / \"Welcome back\"        | \"Create your account\" / \"Log in to [App]\" |\n| Navigation   | None                              | \"Back to login\" link between steps        |\n| Terms        | Footer on both pages              | Inline on signup only                     |\n\n## Copy & Labels\n\n| Screen        | Heading                    | CTA                                                              | Helper                                                                                    |\n| ------------- | -------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |\n| Login method  | Log in to [App Name]       | Continue with Google / Continue with email / Log in with passkey | Don't have an account? Sign up                                                            |\n| Login email   | What's your email address? | Continue with email                                              | ← Back to login                                                                           |\n| Login OTP     | Check your email           | Verify code                                                      | Resend code / ← Back to email                                                             |\n| Signup method | Create your account        | Continue with Google / Continue with email                       | By signing up, you agree to our Terms and Privacy Policy. Already have an account? Log in |\n| Signup email  | What's your email address? | Continue with email                                              | By signing up, you agree to our Terms and Privacy Policy. ← Back to sign up               |\n| Signup OTP    | Check your email           | Verify code                                                      | Resend code / ← Back to email                                                             |\n\n## Component Architecture\n\n### State Machine\n\n```text\n┌─────────────┐     click email      ┌───────────┐    submit email    ┌──────────────┐\n│   METHOD    │ ──────────────────→  │   EMAIL   │ ────────────────→  │     OTP      │\n│  SELECTION  │                      │   INPUT   │                    │ VERIFICATION │\n└─────────────┘  ←───────────────    └───────────┘  ←───────────────  └──────────────┘\n                       back                            back/cancel\n```\n\n### Suggested Step Type\n\n```ts\ntype AuthStep = \"method\" | \"email\" | \"otp\";\n```\n\n### Props\n\n```ts\ninterface AuthFormProps {\n  mode: \"login\" | \"signup\";\n  onSuccess?: () => void;\n}\n```\n\n## Visual Design\n\n- **Layout**: Centered, max-width ~400px, no card wrapper\n- **Logo**: Centered above heading\n- **Buttons**: Full-width, stacked vertically with consistent spacing\n- **Typography**: Clear hierarchy – heading (h1), body text, links\n- **Back link**: Left-aligned, subtle styling, positioned below form\n\n## Transitions\n\n- Smooth fade/slide between steps (optional enhancement)\n- Maintain scroll position when navigating back\n\n## Error Handling\n\n- Inline error messages below relevant input\n- Clear error state when user modifies input\n- Specific messages for common errors (invalid email, expired OTP, rate limit)\n- Third-party auth error surfaced on method selection with a one-line explanation\n\n## Loading & Empty States\n\n- Method selection: disable buttons and show spinner during third-party auth initiation\n- Email input: disable CTA while sending code; show spinner inside button\n- OTP: disable inputs while verifying; show progress indicator\n- Resend: disabled until cooldown expires; show countdown\n\n## OTP Constraints\n\n- 6 digits, numeric only\n- Expires after 10 minutes\n- Resend cooldown: 30 seconds\n- Rate limit: 5 attempts per hour per email\n\n## Accessibility\n\n- Focus management: auto-focus first input when entering email/OTP steps\n- Keyboard navigation: Enter to submit, Escape to go back (optional)\n- Screen reader announcements for step changes\n\n## Open Questions\n\n- [ ] Should the logo link to home or be static?\n- [ ] Add \"Remember me\" checkbox?\n- [ ] Show password option as alternative to OTP?\n- [ ] Magic link option in addition to OTP?\n- [ ] Should login email step include a short notice about email delivery/usage?\n"
  },
  {
    "path": "docs/specs/billing.md",
    "content": "# Stripe Billing Integration\n\n## Overview\n\nStripe billing via `@better-auth/stripe` plugin. Billing is tightly coupled with auth – customer lifecycle, subscription state, and webhook handling are managed by the same system that manages sessions and organizations.\n\n**Non-goals:** Usage-based billing, metered pricing, one-time payments, invoicing, Stripe Elements/embedded checkout, tax calculation, multi-currency. These can be added incrementally.\n\n## Decision Rationale\n\n**Better Auth plugin over raw Stripe SDK:** RSK already uses Better Auth for auth, organizations, and sessions. The plugin handles customer sync, subscription lifecycle, webhook verification, and org-level billing – eliminating significant glue code.\n\n**Hosted Checkout over embedded Elements:** Stripe Checkout is PCI-compliant out of the box, requires no `@stripe/stripe-js` client dependency, and handles payment method selection, 3D Secure, and receipts. The upgrade path to embedded Elements exists but isn't needed initially.\n\n**`createCustomerOnSignUp: true`:** Creates Stripe customer records eagerly on signup. Simplifies the upgrade flow and enables Stripe-side analytics. Trade-off: creates unused records for users who never upgrade.\n\n## Architecture\n\n```text\n┌─────────────┐     POST /api/auth/subscription/upgrade      ┌───────────────┐\n│   Browser   │ ──────────────────────────────────────────→  │  API Worker   │\n│    (app)    │                                              │    (Hono)     │\n│             │  ←── 302 redirect                            │               │\n│             │──→ Stripe Checkout (hosted)                  │  Better Auth  │\n│             │                                              │  + stripe()   │\n│             │    POST /api/auth/stripe/webhook             │  plugin       │\n│             │                                              │               │\n│             │                              Stripe ────────→│  webhook ──→  │\n│             │                                              │  update DB    │\n│             │    GET  /api/trpc/billing.subscription       │               │\n│             │ ──────────────────────────────────────────→  │  tRPC router  │\n└─────────────┘  ←── subscription data (TanStack Query)      └───────────────┘\n```\n\n**Data flow:**\n\n1. User clicks \"Upgrade\" – Better Auth client calls `auth.subscription.upgrade()`\n2. Plugin creates Stripe Checkout session – redirects browser to Stripe\n3. User completes payment – Stripe sends webhook to `/api/auth/stripe/webhook`\n4. Plugin verifies signature, updates `subscription` table – client refetches via tRPC\n\n**Why tRPC for reads, Better Auth client for mutations:**\nSubscription queries benefit from TanStack Query caching, batching, and stale-while-revalidate. Mutations (upgrade, cancel, portal) go through the auth client because the plugin handles Stripe API calls, session validation, and org authorization internally.\n\n## Billing Reference\n\nBilling is tied to `session.activeOrganizationId` when present; otherwise falls back to `user.id` for personal use. The plugin enforces one active subscription per reference ID.\n\n- **Organization context:** `referenceId = activeOrganizationId` – only org owner/admin can manage billing\n- **No organization:** `referenceId = user.id` – user manages their own subscription\n- The server derives `referenceId` from the session – no client-side param needed\n- The billing query key includes `activeOrgId`, so switching organizations automatically fetches fresh billing data via TanStack Query\n\n## Database Schema\n\nThe plugin uses `stripeCustomerId` on the `user` and `organization` tables, and a `subscription` table. The plugin manages the subscription table – no manual inserts/updates needed.\n\nSchema must match plugin expectations. After auth config changes, update the schema in `db/schema/` and run `bun db:generate` to create migrations.\n\n## Plan Configuration\n\nPlan limits defined in `apps/api/lib/plans.ts` (single source of truth), referenced by both auth plugin config and tRPC router. Price IDs come from environment variables (`STRIPE_*_PRICE_ID`).\n\nConfig-as-code is the simplest correct approach – plans rarely change and this makes them testable and version-controlled.\n\n**Escape hatch:** The plugin accepts `plans: () => StripePlan[]` for dynamic plans. Switch to this only when a real use case requires runtime plan management (e.g., admin dashboard for plan CRUD).\n\n**Limits enforcement:** The `limits` object is returned by the `billing.subscription` tRPC query. Enforce limits in application logic (tRPC middleware, UI guards), not in the plugin itself.\n\n## Environment Variables\n\n| Variable                     | Prefix   |\n| ---------------------------- | -------- |\n| `STRIPE_SECRET_KEY`          | `sk_`    |\n| `STRIPE_WEBHOOK_SECRET`      | `whsec_` |\n| `STRIPE_STARTER_PRICE_ID`    | `price_` |\n| `STRIPE_PRO_PRICE_ID`        | `price_` |\n| `STRIPE_PRO_ANNUAL_PRICE_ID` | `price_` |\n\nSet in `.env.local` (local dev), Cloudflare secrets (staging/prod).\n\n## Webhook Setup\n\nThe plugin registers `POST /api/auth/stripe/webhook` automatically. It handles:\n\n- `checkout.session.completed` – activates subscription\n- `customer.subscription.created` – records new subscription\n- `customer.subscription.updated` – syncs status, cancellation scheduling\n- `customer.subscription.deleted` – marks subscription canceled\n\n### Stripe Dashboard Configuration\n\n```\nEndpoint URL: https://<domain>/api/auth/stripe/webhook\nEvents:\n  - checkout.session.completed\n  - customer.subscription.created\n  - customer.subscription.updated\n  - customer.subscription.deleted\n```\n\n### Local Development\n\n```bash\nstripe listen --forward-to localhost:5173/api/auth/stripe/webhook\n# Copy the whsec_... signing secret to .env.local\n```\n\n### Raw Body Requirement\n\nStripe webhook verification requires the raw request body. The plugin handles this via `request.text()` – no special Hono middleware needed.\n\n## Testing\n\nThe plugin tests its own internals (webhooks, checkout, subscription lifecycle, authorization). App tests cover the seams we own:\n\n- **Router** (`apps/api/routers/billing.test.ts`) – free plan fallback, plan limits mapping, unknown plan rejection, response shape\n- **Query** (`apps/app/lib/queries/billing.test.ts`) – cache key includes org ID, null normalization, distinct keys per org, prefix for bulk invalidation\n\nCheckout and webhook flows are not retested at app level – verified via `stripe listen` during development.\n\n## File Map\n\n| Layer  | Files                                                                                                  |\n| ------ | ------------------------------------------------------------------------------------------------------ |\n| Schema | `db/schema/subscription.ts`, `stripeCustomerId` in `db/schema/user.ts` and `db/schema/organization.ts` |\n| Server | `apps/api/lib/plans.ts`, `apps/api/lib/stripe.ts`, stripe plugin in `apps/api/lib/auth.ts`             |\n| Router | `apps/api/routers/billing.ts`, registered in `apps/api/lib/app.ts`                                     |\n| Client | `stripeClient` in `apps/app/lib/auth.ts`, `apps/app/lib/queries/billing.ts`                            |\n| UI     | Billing card in `apps/app/routes/(app)/settings.tsx`                                                   |\n| Tests  | `apps/api/routers/billing.test.ts`, `apps/app/lib/queries/billing.test.ts`                             |\n"
  },
  {
    "path": "docs/specs/infra-terraform.md",
    "content": "# Infrastructure Terraform Specification\n\n## Overview\n\nTwo deployment stacks with clear separation of concerns.\n\n**Non-goals:** Multi-region orchestration, blue-green deployments, auto-scaling policies. These belong in CI/CD or dedicated tooling.\n\n| Stack               | Components                                  | Use Case                |\n| ------------------- | ------------------------------------------- | ----------------------- |\n| **edge** (default)  | Hyperdrive, DNS (Workers via Wrangler)      | Most SaaS apps          |\n| **hybrid** (opt-in) | Cloud Run, Cloud SQL, GCS + optional CF DNS | GCP services, Vertex AI |\n\n## Directory Structure\n\n```bash\ninfra/\n  modules/           # Atomic resources (no credentials)\n    cloudflare/\n      hyperdrive/    # Database connection pooling\n      r2-bucket/     # Object storage\n      dns/           # Proxied DNS records\n    gcp/\n      cloud-run/     # Container deployment\n      cloud-sql/     # Managed PostgreSQL\n      gcs/           # Object storage\n\n  stacks/            # Architectural compositions\n    edge/            # Hyperdrive + DNS (Workers via Wrangler)\n    hybrid/          # GCP + optional CF DNS\n\n  envs/              # Terraform roots (providers + backend + state)\n    dev/edge/\n    preview/edge/\n    staging/edge/\n    prod/edge/\n\n  templates/\n    env-roots/hybrid/        # Copy to enable hybrid\n    backend-r2.example.hcl   # Remote state for edge\n    backend-gcs.example.hcl  # Remote state for hybrid\n```\n\n## Module Contract\n\nModules must NOT define `provider` blocks. Non-HashiCorp providers require `required_providers` to specify the source:\n\n```hcl\n# Cloudflare modules declare source only (no version):\nterraform {\n  required_providers {\n    cloudflare = {\n      source = \"cloudflare/cloudflare\"\n    }\n  }\n}\n```\n\nVersion constraints live exclusively in env roots. This keeps modules reusable while centralizing version management.\n\n## Provider Versions\n\nCanonical versions (single source of truth):\n\n| Provider   | Version        |\n| ---------- | -------------- |\n| terraform  | `>= 1.12, < 2` |\n| cloudflare | `~> 5.0`       |\n| google     | `~> 7.0`       |\n\n## Design Decisions\n\n### Explicit Roots Over Dispatcher\n\nEach `(environment, stack)` pair gets its own Terraform root with isolated state.\n\n```bash\nterraform -chdir=infra/envs/prod/edge apply\n```\n\n**Why not a dispatcher?** A `variable \"stack\"` that switches configs:\n\n- Destroys one stack when switching to another\n- Requires separate backends anyway\n- Creates awkward `module.edge[0].x` references\n\n### No Backend by Default\n\nTerraform uses local state when no backend is configured. Remote backends require pre-existing buckets and credentials.\n\n**Rationale:** Zero-friction onboarding. Add remote backend when ready for team collaboration.\n\n### Providers in Env Roots Only\n\nOnly env roots define `provider` blocks with credentials. Modules declare `required_providers` for source resolution only (no versions, no credentials).\n\n**Rationale:** Keeps modules reusable. Version constraints and credentials stay in one place per environment.\n\n### Preview Uses Edge Only\n\nPR previews need fast spin-up and low cost. Cloudflare Workers: no cold starts, instant deploys, minimal cost.\n\n## Secrets\n\n```bash\n# Via environment variables (CI/CD)\nexport TF_VAR_cloudflare_api_token=\"...\"\nterraform -chdir=infra/envs/prod/edge apply\n\n# Or local terraform.tfvars (gitignored)\n```\n\nMark sensitive variables:\n\n```hcl\nvariable \"cloudflare_api_token\" {\n  type      = string\n  sensitive = true\n}\n```\n\n## Switching to Remote Backend\n\n### Edge Stack (R2)\n\n```bash\ncp infra/templates/backend-r2.example.hcl infra/envs/prod/edge/backend.hcl\nterraform -chdir=infra/envs/prod/edge init -backend-config=backend.hcl -migrate-state\n```\n\n### Hybrid Stack (GCS)\n\n```bash\ncp infra/templates/backend-gcs.example.hcl infra/envs/prod/hybrid/backend.hcl\nterraform -chdir=infra/envs/prod/hybrid init -backend-config=backend.hcl -migrate-state\n```\n\n## Multi-Region\n\nUse separate roots: `envs/prod-eu/edge`, `envs/prod-us/edge`. Each manages its own state.\n\n## Naming Conventions\n\n### Resource values\n\nCloud resources use `{project_slug}-{environment}`; lowercase alphanumeric and hyphens only: `^[a-z0-9-]+$`.\n\n### Resource identifiers\n\nOne simple set of rules:\n\n1. Name the thing being created (provider-native noun, singular).\n\n   ```hcl\n   resource \"cloudflare_hyperdrive_config\" \"hyperdrive\" {}\n   resource \"cloudflare_r2_bucket\"         \"bucket\"     {}\n   resource \"cloudflare_dns_record\"        \"record\"     {}\n   resource \"google_cloud_run_v2_service\"  \"service\"    {}\n   resource \"google_sql_database_instance\" \"instance\"   {}\n   ```\n\n2. If you have multiples, suffix with the role.\n\n   ```hcl\n   resource \"cloudflare_r2_bucket\" \"uploads\" {}\n   resource \"cloudflare_r2_bucket\" \"backups\" {}\n   ```\n\n3. Module names describe architectural role; resource names describe the concrete thing.\n\n   ```hcl\n   module \"hyperdrive\" {\n     # contains: cloudflare_hyperdrive_config.hyperdrive\n   }\n   # → module.hyperdrive.id\n   ```\n\n## Known Limitations\n\n### Hyperdrive Database URL Parsing\n\nThe hyperdrive module parses `database_url` via regex to extract individual connection parameters. This works reliably with Neon URLs (which use URL-safe generated credentials) but has limitations:\n\n- Port must be explicitly specified (e.g., `:5432`)\n- Credentials must not contain unencoded `@` or `:` characters\n- Validation fails fast with a descriptive error message\n\nFor non-Neon databases with special characters in credentials, consider modifying the module to accept individual connection parameters instead.\n"
  },
  {
    "path": "docs/specs/prefixed-ids.md",
    "content": "# Prefixed CUID2 Database IDs\n\nAll database primary keys use application-generated, prefixed [CUID2](https://github.com/paralleldrive/cuid2) identifiers: `usr_ght4k2jxm7pqbv01`. The prefix encodes entity type, improving debuggability across logs, URLs, and support conversations. Same pattern as Stripe (`cus_`, `sub_`), Clerk (`user_`, `org_`).\n\nIDs are opaque strings – clients must not parse or decode them.\n\n## Format\n\n```text\n{prefix}_{body}        Example: usr_ght4k2jxm7pqbv01\n └──3──┘ └─16─┘        20 chars total\n```\n\n- **Prefix:** 3-char lowercase entity type\n- **Body:** 16-char CUID2 (alphanumeric, starts with letter)\n\n## Prefix Map\n\nDefined in `db/schema/id.ts`. Keys are Better Auth model names (not table names).\n\n| Model          | Prefix | Notes                                                   |\n| -------------- | ------ | ------------------------------------------------------- |\n| `user`         | `usr`  |                                                         |\n| `session`      | `ses`  |                                                         |\n| `account`      | `idn`  | Maps to `identity` table via `account.modelName` config |\n| `verification` | `vfy`  |                                                         |\n| `organization` | `org`  |                                                         |\n| `member`       | `mem`  |                                                         |\n| `invitation`   | `inv`  |                                                         |\n| `passkey`      | `pky`  |                                                         |\n| `subscription` | `sub`  |                                                         |\n\n## API\n\n```ts\nimport { generateAuthId, generateId } from \"@repo/db\";\n\n// Auth tables – type-checked against the prefix map\ngenerateAuthId(\"user\"); // \"usr_ght4k2jxm7pqbv01\"\n\n// Non-auth tables – any 3-letter prefix\ngenerateId(\"upl\"); // \"upl_m8xk3jvqp2wnba09\"\n```\n\nThrows on unknown auth models or invalid prefixes. The CUID2 generator is lazy-initialized (no module-level side effects – safe for Workers isolates).\n\n## Integration Points\n\n**Better Auth** – `apps/api/lib/auth.ts`:\n\n```ts\nadvanced: {\n  database: {\n    generateId: ({ model }) => generateAuthId(model as AuthModel),\n  },\n},\n```\n\n**Drizzle schema** – `db/schema/*.ts` use `.$defaultFn()` instead of `gen_random_uuid()`:\n\n```ts\nid: text().primaryKey().$defaultFn(() => generateAuthId(\"user\")),\n```\n\n## Adding a New Model\n\n1. Add the prefix to `AUTH_PREFIX` in `db/schema/id.ts`\n2. Use `.$defaultFn(() => generateAuthId(\"modelName\"))` in the schema\n3. Re-generate migrations: `bun db:generate`\n"
  },
  {
    "path": "docs/testing.md",
    "content": "# Testing\n\nThe project uses [Vitest](https://vitest.dev/) for both API and frontend tests. Two test projects run from a single root config – API tests in Node, frontend tests in [Happy DOM](https://github.com/capricorn86/happy-dom).\n\n## Configuration\n\nThe root config defines both projects:\n\n```ts\n// vitest.config.ts\nexport default defineConfig({\n  test: {\n    projects: [\"apps/api\", \"apps/app\"],\n  },\n});\n```\n\n`apps/api` has its own `vitest.config.ts`; `apps/app` uses an inline `test` block in `vite.config.ts`:\n\n| Project    | Environment    | Setup file        |\n| ---------- | -------------- | ----------------- |\n| `apps/api` | Node (default) | –                 |\n| `apps/app` | `happy-dom`    | `vitest.setup.ts` |\n\nThe app setup file registers [jest-dom](https://github.com/testing-library/jest-dom) matchers like `toBeInTheDocument()`:\n\n```ts\n// apps/app/vitest.setup.ts\nimport \"@testing-library/jest-dom/vitest\";\n```\n\n## Running Tests\n\n```bash\nbun test                       # All projects, watch mode\nbun test --run                 # Single run (no watch)\nbun test --project @repo/api   # API tests only\nbun test --project @repo/app   # Frontend tests only\nbun test billing               # Filter by filename\n```\n\n## File Conventions\n\n- Test files live next to the code they test – `billing.ts` → `billing.test.ts`\n- Import everything from `vitest`, not globals:\n\n```ts\nimport { describe, expect, it, vi } from \"vitest\";\n```\n\n## Testing tRPC Procedures\n\nUse `createCallerFactory` to invoke procedures directly without HTTP. Build a minimal context mock with only the fields the procedure accesses:\n\n```ts\n// apps/api/routers/billing.test.ts\nimport { describe, expect, it, vi } from \"vitest\";\nimport type { TRPCContext } from \"../lib/context\";\nimport { createCallerFactory } from \"../lib/trpc\";\nimport { billingRouter } from \"./billing\";\n\nconst createCaller = createCallerFactory(billingRouter);\n\nfunction testCtx({\n  userId = \"user-1\",\n  activeOrgId = undefined as string | undefined,\n  subscription = undefined as Record<string, unknown> | undefined,\n} = {}) {\n  const ctx: TRPCContext = {\n    req: new Request(\"http://localhost\"),\n    info: {} as TRPCContext[\"info\"],\n    session: {\n      id: \"s-1\",\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      userId,\n      expiresAt: new Date(Date.now() + 60_000),\n      token: \"token\",\n      activeOrganizationId: activeOrgId,\n    },\n    user: {\n      id: userId,\n      createdAt: new Date(),\n      updatedAt: new Date(),\n      email: \"test@example.com\",\n      emailVerified: true,\n      name: \"Test User\",\n    },\n    db: {\n      query: {\n        subscription: {\n          findFirst: vi.fn().mockResolvedValue(subscription),\n        },\n      },\n    } as unknown as TRPCContext[\"db\"],\n    dbDirect: {} as TRPCContext[\"dbDirect\"],\n    cache: new Map(),\n    env: {} as TRPCContext[\"env\"],\n  };\n\n  return ctx;\n}\n\ndescribe(\"billing.subscription\", () => {\n  it(\"returns free plan defaults when no subscription exists\", async () => {\n    const result = await createCaller(testCtx()).subscription();\n    expect(result).toEqual({\n      plan: \"free\",\n      status: null,\n      periodEnd: null,\n      cancelAtPeriodEnd: false,\n      limits: { members: 1 },\n    });\n  });\n\n  it(\"throws on unknown plan name\", async () => {\n    await expect(\n      createCaller(\n        testCtx({ subscription: { plan: \"enterprise\", status: \"active\" } }),\n      ).subscription(),\n    ).rejects.toThrow('Unknown plan \"enterprise\"');\n  });\n});\n```\n\nKey points:\n\n- `createCallerFactory(router)` from `@trpc/server` – calls procedures in-process, no network layer\n- Cast partial DB mocks with `as unknown as TRPCContext[\"db\"]` – only stub the methods your procedure actually calls\n- Use `vi.fn().mockResolvedValue()` for async Drizzle query methods\n\n## Testing Utility Functions\n\nPure functions need no mocking – just import and assert:\n\n```ts\n// apps/app/lib/errors.test.ts\nimport { describe, expect, it } from \"vitest\";\nimport { getErrorMessage, isUnauthenticatedError } from \"./errors\";\n\ndescribe(\"getErrorMessage\", () => {\n  it(\"extracts message from Error instances\", () => {\n    expect(getErrorMessage(new Error(\"Something broke\"))).toBe(\n      \"Something broke\",\n    );\n  });\n\n  it(\"returns fallback for unknown shapes\", () => {\n    expect(getErrorMessage(null)).toBe(\"An unexpected error occurred\");\n  });\n});\n```\n\n## Testing Query Options\n\nTest TanStack Query option factories by inspecting query keys. Use a real `QueryClient` with retries disabled to test cache helpers:\n\n```ts\n// apps/app/lib/queries/session.test.ts\nimport { QueryClient } from \"@tanstack/react-query\";\nimport { describe, expect, it } from \"vitest\";\nimport { getCachedSession, isAuthenticated, sessionQueryKey } from \"./session\";\n\nfunction createQueryClient() {\n  return new QueryClient({\n    defaultOptions: { queries: { retry: false } },\n  });\n}\n\ndescribe(\"isAuthenticated\", () => {\n  it(\"returns true when both user and session exist\", () => {\n    const queryClient = createQueryClient();\n    queryClient.setQueryData(sessionQueryKey, {\n      user: { id: \"user-1\", email: \"test@example.com\" },\n      session: { id: \"session-1\", expiresAt: new Date() },\n    });\n    expect(isAuthenticated(queryClient)).toBe(true);\n  });\n\n  it(\"returns false when no session data cached\", () => {\n    expect(isAuthenticated(createQueryClient())).toBe(false);\n  });\n});\n```\n\n## Testing React Components\n\nThe app project includes [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) with Happy DOM. Components render in a simulated DOM:\n\n```ts\n// apps/app/components/example.test.tsx\nimport { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport { MyComponent } from \"./my-component\";\n\ndescribe(\"MyComponent\", () => {\n  it(\"renders the label\", () => {\n    render(<MyComponent label=\"Hello\" />);\n    expect(screen.getByText(\"Hello\")).toBeInTheDocument();\n  });\n\n  it(\"calls onClick when button is pressed\", async () => {\n    const user = userEvent.setup();\n    const onClick = vi.fn();\n    render(<MyComponent label=\"Click me\" onClick={onClick} />);\n    await user.click(screen.getByRole(\"button\"));\n    expect(onClick).toHaveBeenCalledOnce();\n  });\n});\n```\n\n::: tip\nUse `userEvent` over `fireEvent` for user interactions – it simulates real browser behavior (focus, keyboard events, pointer events) rather than dispatching synthetic events.\n:::\n\n## Mocking\n\n### Function mocks\n\n```ts\nconst fn = vi.fn();\nfn.mockReturnValue(42);\nfn.mockResolvedValue({ data: \"ok\" }); // async\nfn.mockImplementation((x) => x + 1);\n```\n\n### Partial object mocks\n\nCast partial mocks when you only need a subset of a typed interface:\n\n```ts\nconst db = {\n  query: {\n    user: { findFirst: vi.fn().mockResolvedValue({ id: \"user-1\" }) },\n  },\n} as unknown as TRPCContext[\"db\"];\n```\n\n### Module mocks\n\n```ts\nvi.mock(import(\"./some-module.js\"), () => ({\n  myFunction: vi.fn().mockReturnValue(\"mocked\"),\n}));\n```\n\nFor partial module mocks that keep the original implementation:\n\n```ts\nvi.mock(import(\"./some-module.js\"), async (importOriginal) => {\n  const mod = await importOriginal();\n  return { ...mod, myFunction: vi.fn() };\n});\n```\n\n::: warning\nModule mocks are hoisted – they run before imports regardless of where you write them. See [Vitest mocking docs](https://vitest.dev/guide/mocking) for details.\n:::\n\n## Where Tests Live\n\n```\napps/\n├── api/\n│   └── routers/\n│       └── billing.test.ts          # tRPC procedure tests\n└── app/\n    └── lib/\n        ├── errors.test.ts           # utility function tests\n        └── queries/\n            ├── billing.test.ts      # query option tests\n            └── session.test.ts      # cache helper tests\n```\n\nPlace test files next to the source they test. No separate `__tests__` directories.\n"
  },
  {
    "path": "eslint.config.ts",
    "content": "import react from \"@eslint-react/eslint-plugin\";\nimport js from \"@eslint/js\";\nimport * as tsParser from \"@typescript-eslint/parser\";\nimport prettierConfig from \"eslint-config-prettier\";\nimport { defineConfig } from \"eslint/config\";\nimport globals from \"globals\";\nimport ts from \"typescript-eslint\";\n\n/**\n * ESLint configuration.\n * @see https://eslint.org/docs/latest/use/configure/\n */\nexport default defineConfig(\n  // Global ignores\n  {\n    ignores: [\n      \".cache\",\n      \".venv\",\n      \"**/.astro\",\n      \"**/.react-email\",\n      \"**/dist\",\n      \"**/node_modules\",\n      \"docs/.vitepress/cache\",\n      \"docs/.vitepress/dist\",\n    ],\n  },\n\n  // Base configs for all files\n  js.configs.recommended,\n  ...ts.configs.recommended,\n\n  // TypeScript parser for all .ts/.tsx files\n  {\n    files: [\"**/*.{ts,tsx}\"],\n    languageOptions: {\n      parser: tsParser,\n    },\n  },\n\n  // Node.js environment (servers, scripts, config files)\n  {\n    files: [\n      \"**/*.config.{js,ts,mjs}\",\n      \"**/scripts/**/*\",\n      \"apps/api/**/*\",\n      \"apps/email/**/*\",\n      \"db/**/*\",\n      \"infra/**/*\",\n      \"packages/core/**/*\",\n      \"packages/ws-protocol/**/*\",\n    ],\n    languageOptions: {\n      globals: { ...globals.node },\n    },\n  },\n\n  // React environment (frontend apps, email templates)\n  {\n    ...react.configs[\"recommended-typescript\"],\n    files: [\n      \"apps/app/**/*.{ts,tsx}\",\n      \"apps/email/**/*.tsx\",\n      \"apps/web/**/*.{ts,tsx}\",\n      \"packages/ui/**/*.tsx\",\n    ],\n    rules: {\n      ...react.configs[\"recommended-typescript\"].rules,\n      \"@eslint-react/dom/no-missing-iframe-sandbox\": \"off\",\n    },\n    languageOptions: {\n      parser: tsParser,\n      parserOptions: {\n        ecmaVersion: \"latest\",\n        sourceType: \"module\",\n        jsxImportSource: \"react\",\n        ecmaFeatures: { jsx: true },\n      },\n      globals: {\n        ...globals.browser,\n        ...globals.es2021,\n      },\n    },\n  },\n\n  // Email templates: add Node globals (server-side rendering)\n  {\n    files: [\"apps/email/**/*.tsx\"],\n    languageOptions: {\n      globals: { ...globals.node },\n    },\n  },\n\n  // UI package specific overrides\n  {\n    files: [\"packages/ui/**/*.tsx\"],\n    rules: {\n      \"@eslint-react/no-forward-ref\": \"off\",\n    },\n  },\n\n  // Prettier must be last to override any formatting rules\n  prettierConfig,\n);\n"
  },
  {
    "path": "infra/.gitignore",
    "content": ".terraform/\n*.tfstate\n*.tfstate.*\n*.tfvars\n*.tfvars.json\nbackend.hcl\noverride.tf\noverride.tf.json\n*_override.tf\n*_override.tf.json\n.terraform.tfstate.lock.info\ncrash.log\ncrash.*.log\n"
  },
  {
    "path": "infra/README.md",
    "content": "# Infrastructure\n\nTerraform configuration for deploying to Cloudflare (edge) or GCP (hybrid).\n\n[Documentation](https://reactstarter.com/deployment/cloudflare) | [CI/CD](https://reactstarter.com/deployment/ci-cd)\n\n## Design Goals\n\n- **One obvious default:** Edge stack handles most SaaS apps with zero GCP overhead.\n- **Stacks are composable:** Modules have no credentials; stacks wire them together.\n- **State isolation:** Each `envs/<env>/<stack>` directory = one Terraform root = one state file.\n- **Instant code deployments:** Terraform provisions infrastructure; Wrangler deploys code.\n\n## Which Stack?\n\n| Choose **edge** (default) | Choose **hybrid**                  |\n| ------------------------- | ---------------------------------- |\n| Most SaaS apps            | Need GCP services (Vertex AI, etc) |\n| Fastest cold starts       | Require Cloud Run containers       |\n| Minimal cost              | Need Cloud SQL (managed Postgres)  |\n| Neon for database         | Already on GCP                     |\n\n## Structure\n\n```bash\ninfra/\n  modules/         # Atomic resources (no credentials)\n    cloudflare/\n      worker/      # Worker resource (Beta API) - created without code\n      hyperdrive/  # Database connection pooling\n      dns/         # Proxied DNS records\n  stacks/          # Composable architectures\n    edge/          # Workers + Hyperdrive + DNS (routes via Wrangler)\n    hybrid/        # Cloud Run + Cloud SQL + GCS (+ optional CF DNS)\n  envs/            # Terraform roots (providers + backend + state)\n    dev/edge/\n    preview/edge/\n    staging/edge/\n    prod/edge/\n  templates/       # Copy-paste templates for hybrid envs and remote state\n```\n\n## Quickstart (Edge Stack)\n\n```bash\n# Configure variables\ncp infra/envs/dev/edge/terraform.tfvars.example infra/envs/dev/edge/terraform.tfvars\n# Edit terraform.tfvars with your values\n\n# 1. Provision infrastructure (workers, hyperdrive, DNS)\nterraform -chdir=infra/envs/dev/edge init\nterraform -chdir=infra/envs/dev/edge apply\n\n# 2. Copy Hyperdrive ID to apps/api/wrangler.jsonc\nterraform -chdir=infra/envs/dev/edge output hyperdrive_id\n\n# 3. Deploy code + routes via Wrangler\nbun api:deploy    # or: cd apps/api && bun wrangler deploy\nbun app:deploy    # or: cd apps/app && bun wrangler deploy\nbun web:deploy    # or: cd apps/web && bun wrangler deploy (includes routes)\n```\n\nRequired variables: `cloudflare_api_token`, `cloudflare_account_id`, `project_slug`, `environment`, `neon_database_url`\n\nOptional: `cloudflare_zone_id`, `hostname` (for custom domains)\n\nPass secrets via environment variables (recommended for CI/CD):\n\n```bash\nexport TF_VAR_cloudflare_api_token=\"...\"\nexport TF_VAR_neon_database_url=\"$DATABASE_URL\"\nterraform -chdir=infra/envs/dev/edge apply\n```\n\n## Worker Secrets (Wrangler)\n\nTerraform provisions infrastructure only — worker secrets are deployed via Wrangler.\n\n**Required** (all environments):\n\n```bash\nwrangler secret put BETTER_AUTH_SECRET --env <environment>\n```\n\n**Optional** (only if billing is enabled):\n\n```bash\nwrangler secret put STRIPE_SECRET_KEY --env <environment>\nwrangler secret put STRIPE_WEBHOOK_SECRET --env <environment>\nwrangler secret put STRIPE_STARTER_PRICE_ID --env <environment>\nwrangler secret put STRIPE_PRO_PRICE_ID --env <environment>\nwrangler secret put STRIPE_PRO_ANNUAL_PRICE_ID --env <environment>\n```\n\nAfter adding Stripe secrets, register the webhook URL in the Stripe Dashboard:\n\n```bash\nterraform -chdir=infra/envs/<env>/edge output stripe_webhook_url\n```\n\n## Hybrid Stack (GCP)\n\nCopy the hybrid template and configure:\n\n```bash\ncp -r infra/templates/env-roots/hybrid infra/envs/prod/hybrid\n# Edit terraform.tfvars with GCP credentials and settings\nterraform -chdir=infra/envs/prod/hybrid init\nterraform -chdir=infra/envs/prod/hybrid apply\n```\n\n## Remote State\n\nBy default, Terraform uses local state. For team collaboration, configure a remote backend:\n\n```bash\n# Edge stack (R2)\ncp infra/templates/backend-r2.example.hcl infra/envs/prod/edge/backend.hcl\nterraform -chdir=infra/envs/prod/edge init -backend-config=backend.hcl -migrate-state\n\n# Hybrid stack (GCS)\ncp infra/templates/backend-gcs.example.hcl infra/envs/prod/hybrid/backend.hcl\nterraform -chdir=infra/envs/prod/hybrid init -backend-config=backend.hcl -migrate-state\n```\n\n## API Token Permissions (Cloudflare)\n\nTerraform token (infrastructure):\n\n- Zone:DNS:Edit\n- Zone:Zone:Read\n- Account:Workers Scripts:Edit\n- Account:Cloudflare Hyperdrive:Edit\n\nWrangler token (code + routes deployment):\n\n- Zone:Workers Routes:Edit\n- Account:Workers Scripts:Edit\n\n## Requirements\n\n- Terraform >= 1.12\n- Cloudflare provider >= 5.15.0 (for `cloudflare_worker` Beta resource)\n- Cloudflare account (edge stack)\n- GCP project (hybrid stack)\n\nSee `docs/specs/infra-terraform.md` for design details.\n"
  },
  {
    "path": "infra/envs/dev/edge/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/cloudflare/cloudflare\" {\n  version     = \"5.15.0\"\n  constraints = \"~> 5.0, >= 5.15.0\"\n  hashes = [\n    \"h1:EY8mWQO1NTqXuxnZxwVppF7AiLRdx7vRLYk6O7yzgPs=\",\n    \"zh:20a72bdbb28435f11d165b367732369e8f8163100a214e89ad720dae03fafa0c\",\n    \"zh:2eabd7a51fd7aafcab9861631d85c895914857e4fcd6fe2dd80bac22e74a1f47\",\n    \"zh:62828afbc1ba0e0a64bbb7d5d42ae3c2fbbaabb793010b07eba770ba91bae94f\",\n    \"zh:6693f1021e52c34a629300fbcd91f8bd4ca386fda3b45aec746b9c200c28a42c\",\n    \"zh:6873a15454b289e5baecc1d36ce8997266438761386a320753c63f13407f4a6b\",\n    \"zh:afbf4e56b3a5e5950b35b02b553313e4a2008415920b23f536682269c64ca549\",\n    \"zh:db367612900bc2e5a01c6a325e4cff9b1b04960ce9de3dd41671dda5a627ca1d\",\n    \"zh:eb7365eafc6160c3b304a9ce6a598e5400a2e779e9e2bd27976df244f79f774f\",\n    \"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32\",\n  ]\n}\n"
  },
  {
    "path": "infra/envs/dev/edge/main.tf",
    "content": "module \"stack\" {\n  source = \"../../../stacks/edge\"\n\n  cloudflare_account_id = var.cloudflare_account_id\n  cloudflare_zone_id    = var.cloudflare_zone_id\n  hostname              = var.hostname\n  project_slug          = var.project_slug\n  environment           = var.environment\n  neon_database_url     = var.neon_database_url\n}\n\noutput \"worker_api_name\" {\n  value = module.stack.worker_api_name\n}\n\noutput \"worker_app_name\" {\n  value = module.stack.worker_app_name\n}\n\noutput \"worker_web_name\" {\n  value = module.stack.worker_web_name\n}\n\noutput \"hyperdrive_id\" {\n  value = module.stack.hyperdrive_id\n}\n\noutput \"hyperdrive_name\" {\n  value = module.stack.hyperdrive_name\n}\n"
  },
  {
    "path": "infra/envs/dev/edge/providers.tf",
    "content": "terraform {\n  required_version = \">= 1.12, < 2.0\"\n\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 5.0, >= 5.15.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n"
  },
  {
    "path": "infra/envs/dev/edge/terraform.tfvars.example",
    "content": "cloudflare_api_token  = \"\"\ncloudflare_account_id = \"\"\ncloudflare_zone_id    = \"\"\nhostname              = \"\"\nproject_slug          = \"myapp\"\nenvironment           = \"dev\"\nneon_database_url     = \"\"\n"
  },
  {
    "path": "infra/envs/dev/edge/variables.tf",
    "content": "variable \"cloudflare_api_token\" {\n  type      = string\n  sensitive = true\n}\n\nvariable \"cloudflare_account_id\" {\n  type = string\n}\n\nvariable \"cloudflare_zone_id\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"hostname\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"project_slug\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type = string\n}\n\nvariable \"neon_database_url\" {\n  type      = string\n  sensitive = true\n}\n"
  },
  {
    "path": "infra/envs/preview/edge/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/cloudflare/cloudflare\" {\n  version     = \"5.15.0\"\n  constraints = \"~> 5.0, >= 5.15.0\"\n  hashes = [\n    \"h1:EY8mWQO1NTqXuxnZxwVppF7AiLRdx7vRLYk6O7yzgPs=\",\n    \"zh:20a72bdbb28435f11d165b367732369e8f8163100a214e89ad720dae03fafa0c\",\n    \"zh:2eabd7a51fd7aafcab9861631d85c895914857e4fcd6fe2dd80bac22e74a1f47\",\n    \"zh:62828afbc1ba0e0a64bbb7d5d42ae3c2fbbaabb793010b07eba770ba91bae94f\",\n    \"zh:6693f1021e52c34a629300fbcd91f8bd4ca386fda3b45aec746b9c200c28a42c\",\n    \"zh:6873a15454b289e5baecc1d36ce8997266438761386a320753c63f13407f4a6b\",\n    \"zh:afbf4e56b3a5e5950b35b02b553313e4a2008415920b23f536682269c64ca549\",\n    \"zh:db367612900bc2e5a01c6a325e4cff9b1b04960ce9de3dd41671dda5a627ca1d\",\n    \"zh:eb7365eafc6160c3b304a9ce6a598e5400a2e779e9e2bd27976df244f79f774f\",\n    \"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32\",\n  ]\n}\n"
  },
  {
    "path": "infra/envs/preview/edge/main.tf",
    "content": "module \"stack\" {\n  source = \"../../../stacks/edge\"\n\n  cloudflare_account_id = var.cloudflare_account_id\n  cloudflare_zone_id    = var.cloudflare_zone_id\n  hostname              = var.hostname\n  project_slug          = var.project_slug\n  environment           = var.environment\n  neon_database_url     = var.neon_database_url\n}\n\noutput \"worker_api_name\" {\n  value = module.stack.worker_api_name\n}\n\noutput \"worker_app_name\" {\n  value = module.stack.worker_app_name\n}\n\noutput \"worker_web_name\" {\n  value = module.stack.worker_web_name\n}\n\noutput \"hyperdrive_id\" {\n  value = module.stack.hyperdrive_id\n}\n\noutput \"hyperdrive_name\" {\n  value = module.stack.hyperdrive_name\n}\n"
  },
  {
    "path": "infra/envs/preview/edge/providers.tf",
    "content": "terraform {\n  required_version = \">= 1.12, < 2.0\"\n\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 5.0, >= 5.15.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n"
  },
  {
    "path": "infra/envs/preview/edge/terraform.tfvars.example",
    "content": "cloudflare_api_token  = \"\"\ncloudflare_account_id = \"\"\ncloudflare_zone_id    = \"\"\nhostname              = \"\"\nproject_slug          = \"myapp\"\nenvironment           = \"preview\"\nneon_database_url     = \"\"\n"
  },
  {
    "path": "infra/envs/preview/edge/variables.tf",
    "content": "variable \"cloudflare_api_token\" {\n  type      = string\n  sensitive = true\n}\n\nvariable \"cloudflare_account_id\" {\n  type = string\n}\n\nvariable \"cloudflare_zone_id\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"hostname\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"project_slug\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type = string\n}\n\nvariable \"neon_database_url\" {\n  type      = string\n  sensitive = true\n}\n"
  },
  {
    "path": "infra/envs/prod/edge/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/cloudflare/cloudflare\" {\n  version     = \"5.15.0\"\n  constraints = \"~> 5.0, >= 5.15.0\"\n  hashes = [\n    \"h1:EY8mWQO1NTqXuxnZxwVppF7AiLRdx7vRLYk6O7yzgPs=\",\n    \"zh:20a72bdbb28435f11d165b367732369e8f8163100a214e89ad720dae03fafa0c\",\n    \"zh:2eabd7a51fd7aafcab9861631d85c895914857e4fcd6fe2dd80bac22e74a1f47\",\n    \"zh:62828afbc1ba0e0a64bbb7d5d42ae3c2fbbaabb793010b07eba770ba91bae94f\",\n    \"zh:6693f1021e52c34a629300fbcd91f8bd4ca386fda3b45aec746b9c200c28a42c\",\n    \"zh:6873a15454b289e5baecc1d36ce8997266438761386a320753c63f13407f4a6b\",\n    \"zh:afbf4e56b3a5e5950b35b02b553313e4a2008415920b23f536682269c64ca549\",\n    \"zh:db367612900bc2e5a01c6a325e4cff9b1b04960ce9de3dd41671dda5a627ca1d\",\n    \"zh:eb7365eafc6160c3b304a9ce6a598e5400a2e779e9e2bd27976df244f79f774f\",\n    \"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32\",\n  ]\n}\n"
  },
  {
    "path": "infra/envs/prod/edge/main.tf",
    "content": "module \"stack\" {\n  source = \"../../../stacks/edge\"\n\n  cloudflare_account_id = var.cloudflare_account_id\n  cloudflare_zone_id    = var.cloudflare_zone_id\n  hostname              = var.hostname\n  project_slug          = var.project_slug\n  environment           = var.environment\n  neon_database_url     = var.neon_database_url\n}\n\noutput \"worker_api_name\" {\n  value = module.stack.worker_api_name\n}\n\noutput \"worker_app_name\" {\n  value = module.stack.worker_app_name\n}\n\noutput \"worker_web_name\" {\n  value = module.stack.worker_web_name\n}\n\noutput \"hyperdrive_id\" {\n  value = module.stack.hyperdrive_id\n}\n\noutput \"hyperdrive_name\" {\n  value = module.stack.hyperdrive_name\n}\n"
  },
  {
    "path": "infra/envs/prod/edge/providers.tf",
    "content": "terraform {\n  required_version = \">= 1.12, < 2.0\"\n\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 5.0, >= 5.15.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n"
  },
  {
    "path": "infra/envs/prod/edge/terraform.tfvars.example",
    "content": "cloudflare_api_token  = \"\"\ncloudflare_account_id = \"\"\ncloudflare_zone_id    = \"\"\nhostname              = \"\"\nproject_slug          = \"myapp\"\nenvironment           = \"prod\"\nneon_database_url     = \"\"\n"
  },
  {
    "path": "infra/envs/prod/edge/variables.tf",
    "content": "variable \"cloudflare_api_token\" {\n  type      = string\n  sensitive = true\n}\n\nvariable \"cloudflare_account_id\" {\n  type = string\n}\n\nvariable \"cloudflare_zone_id\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"hostname\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"project_slug\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type = string\n}\n\nvariable \"neon_database_url\" {\n  type      = string\n  sensitive = true\n}\n"
  },
  {
    "path": "infra/envs/staging/edge/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/cloudflare/cloudflare\" {\n  version     = \"5.15.0\"\n  constraints = \"~> 5.0, >= 5.15.0\"\n  hashes = [\n    \"h1:EY8mWQO1NTqXuxnZxwVppF7AiLRdx7vRLYk6O7yzgPs=\",\n    \"zh:20a72bdbb28435f11d165b367732369e8f8163100a214e89ad720dae03fafa0c\",\n    \"zh:2eabd7a51fd7aafcab9861631d85c895914857e4fcd6fe2dd80bac22e74a1f47\",\n    \"zh:62828afbc1ba0e0a64bbb7d5d42ae3c2fbbaabb793010b07eba770ba91bae94f\",\n    \"zh:6693f1021e52c34a629300fbcd91f8bd4ca386fda3b45aec746b9c200c28a42c\",\n    \"zh:6873a15454b289e5baecc1d36ce8997266438761386a320753c63f13407f4a6b\",\n    \"zh:afbf4e56b3a5e5950b35b02b553313e4a2008415920b23f536682269c64ca549\",\n    \"zh:db367612900bc2e5a01c6a325e4cff9b1b04960ce9de3dd41671dda5a627ca1d\",\n    \"zh:eb7365eafc6160c3b304a9ce6a598e5400a2e779e9e2bd27976df244f79f774f\",\n    \"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32\",\n  ]\n}\n"
  },
  {
    "path": "infra/envs/staging/edge/main.tf",
    "content": "module \"stack\" {\n  source = \"../../../stacks/edge\"\n\n  cloudflare_account_id = var.cloudflare_account_id\n  cloudflare_zone_id    = var.cloudflare_zone_id\n  hostname              = var.hostname\n  project_slug          = var.project_slug\n  environment           = var.environment\n  neon_database_url     = var.neon_database_url\n}\n\noutput \"worker_api_name\" {\n  value = module.stack.worker_api_name\n}\n\noutput \"worker_app_name\" {\n  value = module.stack.worker_app_name\n}\n\noutput \"worker_web_name\" {\n  value = module.stack.worker_web_name\n}\n\noutput \"hyperdrive_id\" {\n  value = module.stack.hyperdrive_id\n}\n\noutput \"hyperdrive_name\" {\n  value = module.stack.hyperdrive_name\n}\n"
  },
  {
    "path": "infra/envs/staging/edge/providers.tf",
    "content": "terraform {\n  required_version = \">= 1.12, < 2.0\"\n\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 5.0, >= 5.15.0\"\n    }\n  }\n}\n\nprovider \"cloudflare\" {\n  api_token = var.cloudflare_api_token\n}\n"
  },
  {
    "path": "infra/envs/staging/edge/terraform.tfvars.example",
    "content": "cloudflare_api_token  = \"\"\ncloudflare_account_id = \"\"\ncloudflare_zone_id    = \"\"\nhostname              = \"\"\nproject_slug          = \"myapp\"\nenvironment           = \"staging\"\nneon_database_url     = \"\"\n"
  },
  {
    "path": "infra/envs/staging/edge/variables.tf",
    "content": "variable \"cloudflare_api_token\" {\n  type      = string\n  sensitive = true\n}\n\nvariable \"cloudflare_account_id\" {\n  type = string\n}\n\nvariable \"cloudflare_zone_id\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"hostname\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"project_slug\" {\n  type = string\n}\n\nvariable \"environment\" {\n  type = string\n}\n\nvariable \"neon_database_url\" {\n  type      = string\n  sensitive = true\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/dns/main.tf",
    "content": "# Proxied DNS record for Cloudflare Workers routing.\n# Workers and routes are managed via Wrangler, not Terraform.\n\nterraform {\n  required_providers {\n    cloudflare = {\n      source = \"cloudflare/cloudflare\"\n    }\n  }\n}\n\nresource \"cloudflare_dns_record\" \"record\" {\n  zone_id = var.zone_id\n  name    = var.hostname\n  type    = \"AAAA\"\n  content = \"100::\"\n  ttl     = 1 # Auto (required for proxied records)\n  proxied = true\n  comment = \"Managed by Terraform\"\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/dns/outputs.tf",
    "content": "output \"hostname\" {\n  description = \"The configured hostname\"\n  value       = var.hostname\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/dns/variables.tf",
    "content": "variable \"zone_id\" {\n  description = \"Cloudflare zone ID\"\n  type        = string\n}\n\nvariable \"hostname\" {\n  description = \"DNS hostname (e.g., 'example.com' or 'staging.example.com')\"\n  type        = string\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/hyperdrive/main.tf",
    "content": "terraform {\n  required_providers {\n    cloudflare = {\n      source = \"cloudflare/cloudflare\"\n    }\n  }\n}\n\nlocals {\n  # Normalize postgresql:// to postgres:// for regex parsing.\n  # Limitation: credentials must not contain unencoded @ or : characters.\n  # This works reliably with Neon URLs which use URL-safe generated credentials.\n  db_url = replace(var.database_url, \"postgresql://\", \"postgres://\")\n}\n\nresource \"cloudflare_hyperdrive_config\" \"hyperdrive\" {\n  account_id = var.account_id\n  name       = var.name\n\n  mtls = {}\n\n  origin = {\n    database = regex(\"^postgres://[^:]+:[^@]+@[^:/]+:[0-9]+/([^?]+)\", local.db_url)[0]\n    password = regex(\"^postgres://[^:]+:([^@]+)@\", local.db_url)[0]\n    host     = regex(\"^postgres://[^:]+:[^@]+@([^:/]+):\", local.db_url)[0]\n    port     = tonumber(regex(\"^postgres://[^:]+:[^@]+@[^:/]+:([0-9]+)/\", local.db_url)[0])\n    scheme   = \"postgres\"\n    user     = regex(\"^postgres://([^:]+):\", local.db_url)[0]\n  }\n\n  origin_connection_limit = 60\n\n  caching = {\n    disabled = true\n  }\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/hyperdrive/outputs.tf",
    "content": "output \"id\" {\n  description = \"Hyperdrive configuration ID\"\n  value       = cloudflare_hyperdrive_config.hyperdrive.id\n}\n\noutput \"name\" {\n  description = \"Hyperdrive configuration name\"\n  value       = cloudflare_hyperdrive_config.hyperdrive.name\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/hyperdrive/variables.tf",
    "content": "variable \"account_id\" {\n  description = \"Cloudflare account ID\"\n  type        = string\n}\n\nvariable \"name\" {\n  description = \"Hyperdrive configuration name\"\n  type        = string\n}\n\nvariable \"database_url\" {\n  description = \"PostgreSQL connection URL in format postgres://user:pass@host:port/db (port required, no special chars in credentials)\"\n  type        = string\n  sensitive   = true\n\n  validation {\n    condition     = can(regex(\"^postgres(ql)?://[^:]+:[^@]+@[^:/]+:[0-9]+/[^?]+\", var.database_url))\n    error_message = \"Invalid database_url format. Expected: postgres://user:pass@host:port/db (port required, credentials must not contain unencoded @ or :)\"\n  }\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/r2-bucket/main.tf",
    "content": "terraform {\n  required_providers {\n    cloudflare = {\n      source = \"cloudflare/cloudflare\"\n    }\n  }\n}\n\nresource \"cloudflare_r2_bucket\" \"bucket\" {\n  account_id = var.account_id\n  name       = var.name\n  location   = var.location\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/r2-bucket/outputs.tf",
    "content": "output \"name\" {\n  description = \"R2 bucket name\"\n  value       = cloudflare_r2_bucket.bucket.name\n}\n\noutput \"id\" {\n  description = \"R2 bucket ID\"\n  value       = cloudflare_r2_bucket.bucket.id\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/r2-bucket/variables.tf",
    "content": "variable \"account_id\" {\n  description = \"Cloudflare account ID\"\n  type        = string\n}\n\nvariable \"name\" {\n  description = \"R2 bucket name\"\n  type        = string\n}\n\nvariable \"location\" {\n  description = \"R2 bucket location\"\n  type        = string\n  default     = \"enam\"\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/worker/main.tf",
    "content": "# Worker resource without code. Deploy via Wrangler.\n# Routes managed via wrangler.jsonc (routes live with code, not infra).\n\nterraform {\n  required_providers {\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \">= 5.15.0\"\n    }\n  }\n}\n\nresource \"cloudflare_worker\" \"worker\" {\n  account_id = var.account_id\n  name       = var.name\n\n  observability = var.observability_enabled ? {\n    enabled            = true\n    head_sampling_rate = var.head_sampling_rate\n  } : null\n\n  subdomain = var.subdomain_enabled ? {\n    enabled          = true\n    previews_enabled = var.previews_enabled\n  } : null\n\n  tags = var.tags\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/worker/outputs.tf",
    "content": "output \"id\" {\n  value       = cloudflare_worker.worker.id\n  description = \"Worker UUID\"\n}\n\noutput \"name\" {\n  value       = cloudflare_worker.worker.name\n  description = \"Worker name\"\n}\n\noutput \"subdomain_url\" {\n  value       = var.subdomain_enabled ? \"https://${cloudflare_worker.worker.name}.workers.dev\" : null\n  description = \"Workers.dev URL (null if subdomain disabled)\"\n}\n"
  },
  {
    "path": "infra/modules/cloudflare/worker/variables.tf",
    "content": "variable \"account_id\" {\n  type        = string\n  description = \"Cloudflare account ID\"\n}\n\nvariable \"name\" {\n  type        = string\n  description = \"Worker name (used in URLs and route configuration)\"\n}\n\nvariable \"observability_enabled\" {\n  type        = bool\n  description = \"Enable observability (logs and metrics)\"\n  default     = true\n}\n\nvariable \"head_sampling_rate\" {\n  type        = number\n  description = \"Sampling rate for head-based sampling (0.0 to 1.0)\"\n  default     = 1\n}\n\nvariable \"subdomain_enabled\" {\n  type        = bool\n  description = \"Enable workers.dev subdomain\"\n  default     = false\n}\n\nvariable \"previews_enabled\" {\n  type        = bool\n  description = \"Enable preview deployments on workers.dev subdomain\"\n  default     = false\n}\n\nvariable \"tags\" {\n  type        = list(string)\n  description = \"Tags for organizing workers\"\n  default     = []\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-run/main.tf",
    "content": "resource \"google_cloud_run_v2_service\" \"service\" {\n  name                 = var.service_name\n  location             = var.region\n  deletion_protection  = false\n  ingress              = \"INGRESS_TRAFFIC_ALL\"\n  invoker_iam_disabled = true # Allow public access without IAM checks\n\n  template {\n    scaling {\n      min_instance_count = 0\n      max_instance_count = 10\n    }\n\n    dynamic \"volumes\" {\n      for_each = var.cloud_sql_connection != null ? [1] : []\n      content {\n        name = \"cloudsql\"\n        cloud_sql_instance {\n          instances = [var.cloud_sql_connection]\n        }\n      }\n    }\n\n    containers {\n      image = var.image\n\n      ports {\n        container_port = 8080\n      }\n\n      resources {\n        limits = {\n          cpu    = \"1\"\n          memory = \"512Mi\"\n        }\n        cpu_idle = true\n      }\n\n      dynamic \"volume_mounts\" {\n        for_each = var.cloud_sql_connection != null ? [1] : []\n        content {\n          name       = \"cloudsql\"\n          mount_path = \"/cloudsql\"\n        }\n      }\n\n      dynamic \"env\" {\n        for_each = var.env_vars\n        content {\n          name  = env.key\n          value = env.value\n        }\n      }\n    }\n  }\n\n  lifecycle {\n    ignore_changes = [\n      template[0].containers[0].image, # Allow gcloud to deploy new images\n      template[0].revision,\n      template[0].labels,\n    ]\n  }\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-run/outputs.tf",
    "content": "output \"url\" {\n  description = \"URL of the Cloud Run service\"\n  value       = google_cloud_run_v2_service.service.uri\n}\n\noutput \"service_name\" {\n  description = \"Name of the Cloud Run service\"\n  value       = google_cloud_run_v2_service.service.name\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-run/variables.tf",
    "content": "variable \"project_id\" {\n  description = \"GCP project ID\"\n  type        = string\n}\n\nvariable \"region\" {\n  description = \"GCP region for Cloud Run service\"\n  type        = string\n}\n\nvariable \"service_name\" {\n  description = \"Name of the Cloud Run service\"\n  type        = string\n}\n\nvariable \"image\" {\n  description = \"Container image to deploy\"\n  type        = string\n}\n\nvariable \"env_vars\" {\n  description = \"Environment variables for the service\"\n  type        = map(string)\n  default     = {}\n}\n\nvariable \"cloud_sql_connection\" {\n  description = \"Cloud SQL connection name (project:region:instance)\"\n  type        = string\n  default     = null\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-sql/main.tf",
    "content": "resource \"google_sql_database_instance\" \"instance\" {\n  name             = var.instance_name\n  database_version = \"POSTGRES_18\"\n\n  settings {\n    edition = \"ENTERPRISE\"\n    tier    = var.tier\n\n    disk_size = 10\n    disk_type = \"PD_SSD\"\n\n    ip_configuration {\n      ipv4_enabled    = var.private_network_id == null\n      private_network = var.private_network_id\n    }\n\n    backup_configuration {\n      enabled = false\n    }\n  }\n}\n\nresource \"google_sql_database\" \"database\" {\n  name     = var.database_name\n  instance = google_sql_database_instance.instance.name\n}\n\nresource \"random_password\" \"password\" {\n  length           = 32\n  special          = true\n  override_special = \"-_\"\n}\n\nresource \"google_sql_user\" \"user\" {\n  name     = var.database_name\n  instance = google_sql_database_instance.instance.name\n  password = random_password.password.result\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-sql/outputs.tf",
    "content": "output \"connection_name\" {\n  description = \"Cloud SQL connection name (project:region:instance)\"\n  value       = google_sql_database_instance.instance.connection_name\n}\n\noutput \"connection_string\" {\n  description = \"PostgreSQL connection string\"\n  value       = \"postgresql://${google_sql_user.user.name}:${random_password.password.result}@localhost/${var.database_name}?host=/cloudsql/${google_sql_database_instance.instance.connection_name}\"\n  sensitive   = true\n}\n\noutput \"instance_ip\" {\n  description = \"Private IP address of the instance\"\n  value       = google_sql_database_instance.instance.private_ip_address\n}\n"
  },
  {
    "path": "infra/modules/gcp/cloud-sql/variables.tf",
    "content": "variable \"project_id\" {\n  description = \"GCP project ID\"\n  type        = string\n}\n\nvariable \"region\" {\n  description = \"GCP region for Cloud SQL instance\"\n  type        = string\n}\n\nvariable \"instance_name\" {\n  description = \"Name of the Cloud SQL instance\"\n  type        = string\n}\n\nvariable \"tier\" {\n  description = \"Machine tier for Cloud SQL instance\"\n  type        = string\n  default     = \"db-f1-micro\"\n}\n\nvariable \"database_name\" {\n  description = \"Name of the database to create\"\n  type        = string\n}\n\nvariable \"private_network_id\" {\n  description = \"VPC network ID for private IP (optional, enables public IP if not set)\"\n  type        = string\n  default     = null\n}\n"
  },
  {
    "path": "infra/modules/gcp/gcs/main.tf",
    "content": "resource \"google_storage_bucket\" \"bucket\" {\n  name          = var.bucket_name\n  location      = var.location\n  force_destroy = false\n\n  uniform_bucket_level_access = true\n\n  dynamic \"cors\" {\n    for_each = length(var.cors_origins) > 0 ? [1] : []\n    content {\n      origin = var.cors_origins\n      method = [\"GET\", \"PUT\", \"POST\", \"OPTIONS\"]\n      response_header = [\n        \"Content-Type\",\n        \"Access-Control-Allow-Origin\",\n        \"x-goog-resumable\"\n      ]\n      max_age_seconds = 3600\n    }\n  }\n}\n"
  },
  {
    "path": "infra/modules/gcp/gcs/outputs.tf",
    "content": "output \"bucket_name\" {\n  description = \"Name of the GCS bucket\"\n  value       = google_storage_bucket.bucket.name\n}\n\noutput \"url\" {\n  description = \"URL of the GCS bucket\"\n  value       = google_storage_bucket.bucket.url\n}\n"
  },
  {
    "path": "infra/modules/gcp/gcs/variables.tf",
    "content": "variable \"project_id\" {\n  description = \"GCP project ID\"\n  type        = string\n}\n\nvariable \"location\" {\n  description = \"GCS bucket location\"\n  type        = string\n}\n\nvariable \"bucket_name\" {\n  description = \"Name of the GCS bucket\"\n  type        = string\n}\n\nvariable \"cors_origins\" {\n  description = \"List of CORS origins\"\n  type        = list(string)\n  default     = []\n}\n"
  },
  {
    "path": "infra/stacks/edge/main.tf",
    "content": "# Edge stack: Cloudflare infrastructure for Workers deployment.\n# Worker metadata created here; code + routes deployed via Wrangler.\n\nlocals {\n  worker_suffix     = var.environment == \"prod\" ? \"\" : \"-${var.environment}\"\n  has_custom_domain = var.cloudflare_zone_id != \"\" && var.hostname != \"\"\n}\n\n# API Worker (tRPC, auth endpoints)\nmodule \"worker_api\" {\n  source = \"../../modules/cloudflare/worker\"\n\n  account_id            = var.cloudflare_account_id\n  name                  = \"${var.project_slug}-api${local.worker_suffix}\"\n  observability_enabled = true\n  subdomain_enabled     = !local.has_custom_domain\n\n  tags = [var.project_slug, var.environment]\n}\n\n# App Worker (SPA with static assets)\nmodule \"worker_app\" {\n  source = \"../../modules/cloudflare/worker\"\n\n  account_id            = var.cloudflare_account_id\n  name                  = \"${var.project_slug}-app${local.worker_suffix}\"\n  observability_enabled = true\n  subdomain_enabled     = !local.has_custom_domain\n\n  tags = [var.project_slug, var.environment]\n}\n\n# Web Worker (marketing site, edge router)\nmodule \"worker_web\" {\n  source = \"../../modules/cloudflare/worker\"\n\n  account_id            = var.cloudflare_account_id\n  name                  = \"${var.project_slug}-web${local.worker_suffix}\"\n  observability_enabled = true\n  subdomain_enabled     = !local.has_custom_domain\n\n  tags = [var.project_slug, var.environment]\n}\n\nmodule \"hyperdrive\" {\n  source = \"../../modules/cloudflare/hyperdrive\"\n\n  account_id   = var.cloudflare_account_id\n  name         = \"${var.project_slug}-${var.environment}\"\n  database_url = var.neon_database_url\n}\n\nmodule \"dns\" {\n  count  = local.has_custom_domain ? 1 : 0\n  source = \"../../modules/cloudflare/dns\"\n\n  zone_id  = var.cloudflare_zone_id\n  hostname = var.hostname\n}\n"
  },
  {
    "path": "infra/stacks/edge/outputs.tf",
    "content": "# Worker names for wrangler deploy\noutput \"worker_api_name\" {\n  value       = module.worker_api.name\n  description = \"API worker name for wrangler deploy\"\n}\n\noutput \"worker_app_name\" {\n  value       = module.worker_app.name\n  description = \"App worker name for wrangler deploy\"\n}\n\noutput \"worker_web_name\" {\n  value       = module.worker_web.name\n  description = \"Web worker name for wrangler deploy\"\n}\n\n# Hyperdrive ID for wrangler.jsonc\noutput \"hyperdrive_id\" {\n  value       = module.hyperdrive.id\n  description = \"Hyperdrive configuration ID for wrangler.jsonc\"\n}\n\noutput \"hyperdrive_name\" {\n  value       = module.hyperdrive.name\n  description = \"Hyperdrive configuration name\"\n}\n\noutput \"hostname\" {\n  value       = var.hostname != \"\" ? var.hostname : null\n  description = \"Configured hostname (null if using workers.dev)\"\n}\n\n# Stripe webhook URL for dashboard configuration\noutput \"stripe_webhook_url\" {\n  value       = \"https://${var.hostname != \"\" ? var.hostname : \"${module.worker_api.name}.workers.dev\"}/api/auth/stripe/webhook\"\n  description = \"Register in Stripe Dashboard → Webhooks (only needed if billing is enabled)\"\n}\n"
  },
  {
    "path": "infra/stacks/edge/variables.tf",
    "content": "variable \"cloudflare_account_id\" {\n  type        = string\n  description = \"Cloudflare account ID\"\n}\n\nvariable \"cloudflare_zone_id\" {\n  type        = string\n  description = \"Cloudflare zone ID (required when hostname is set)\"\n  default     = \"\"\n}\n\nvariable \"hostname\" {\n  type        = string\n  description = \"Public hostname (e.g., example.com). If empty, uses workers.dev URLs.\"\n  default     = \"\"\n}\n\nvariable \"project_slug\" {\n  type        = string\n  description = \"Short identifier for resource naming (e.g., myapp)\"\n}\n\nvariable \"environment\" {\n  type        = string\n  description = \"Environment name (e.g., dev, staging, prod)\"\n}\n\nvariable \"neon_database_url\" {\n  type        = string\n  description = \"Neon PostgreSQL connection string\"\n  sensitive   = true\n}\n"
  },
  {
    "path": "infra/stacks/hybrid/main.tf",
    "content": "# Hybrid stack: GCP backend with optional Cloudflare edge.\n# Workers are deployed separately via Wrangler.\n\n# Cloud SQL PostgreSQL\nmodule \"database\" {\n  source = \"../../modules/gcp/cloud-sql\"\n\n  project_id    = var.gcp_project_id\n  region        = var.gcp_region\n  instance_name = \"${var.project_slug}-${var.environment}\"\n  database_name = var.project_slug\n  tier          = var.cloud_sql_tier\n}\n\n# GCS bucket for uploads\nmodule \"storage\" {\n  source = \"../../modules/gcp/gcs\"\n\n  project_id  = var.gcp_project_id\n  location    = var.gcp_region\n  bucket_name = \"${var.project_slug}-${var.environment}-uploads\"\n}\n\n# Cloud Run API service\nmodule \"api\" {\n  source = \"../../modules/gcp/cloud-run\"\n\n  project_id   = var.gcp_project_id\n  region       = var.gcp_region\n  service_name = \"${var.project_slug}-api-${var.environment}\"\n  image        = var.api_image\n\n  cloud_sql_connection = module.database.connection_name\n\n  env_vars = {\n    DATABASE_URL = module.database.connection_string\n    GCS_BUCKET   = module.storage.bucket_name\n  }\n}\n\n# Optional: Cloudflare DNS for edge routing.\n# Deploy the edge proxy Worker separately via Wrangler.\nmodule \"dns\" {\n  count  = var.enable_edge_routing && var.hostname != \"\" ? 1 : 0\n  source = \"../../modules/cloudflare/dns\"\n\n  zone_id  = var.cloudflare_zone_id\n  hostname = var.hostname\n}\n"
  },
  {
    "path": "infra/stacks/hybrid/outputs.tf",
    "content": "output \"api_url\" {\n  value       = module.api.url\n  description = \"Cloud Run service URL\"\n}\n\noutput \"edge_api_url\" {\n  value       = var.enable_edge_routing && var.hostname != \"\" ? \"https://${var.hostname}/api\" : null\n  description = \"Public API URL via Cloudflare edge (null if edge routing disabled or no hostname)\"\n}\n"
  },
  {
    "path": "infra/stacks/hybrid/variables.tf",
    "content": "variable \"gcp_project_id\" {\n  type        = string\n  description = \"GCP project ID\"\n}\n\nvariable \"gcp_region\" {\n  type        = string\n  description = \"GCP region (e.g., us-central1)\"\n}\n\nvariable \"project_slug\" {\n  type        = string\n  description = \"Short identifier for resource naming (e.g., myapp)\"\n}\n\nvariable \"environment\" {\n  type        = string\n  description = \"Environment name (e.g., dev, staging, prod)\"\n}\n\nvariable \"api_image\" {\n  type        = string\n  description = \"Container image URL for Cloud Run API service\"\n}\n\nvariable \"cloud_sql_tier\" {\n  type        = string\n  description = \"Cloud SQL instance tier (e.g., db-f1-micro)\"\n  default     = \"db-f1-micro\"\n}\n\nvariable \"enable_edge_routing\" {\n  type        = bool\n  description = \"Enable Cloudflare edge routing in front of Cloud Run\"\n  default     = false\n}\n\nvariable \"cloudflare_zone_id\" {\n  type        = string\n  description = \"Cloudflare zone ID (required when enable_edge_routing = true and hostname is set)\"\n  default     = \"\"\n}\n\nvariable \"hostname\" {\n  type        = string\n  description = \"Public hostname for edge routing (e.g., api-gcp.example.com)\"\n  default     = \"\"\n}\n"
  },
  {
    "path": "infra/templates/backend-gcs.example.hcl",
    "content": "bucket = \"tf-state\"\nprefix = \"prod/hybrid\"\n"
  },
  {
    "path": "infra/templates/backend-r2.example.hcl",
    "content": "bucket = \"tf-state\"\nkey    = \"prod/edge/terraform.tfstate\"\n\nendpoints = {\n  s3 = \"https://<ACCOUNT_ID>.r2.cloudflarestorage.com\"\n}\n\n# Do not hard-code secrets here. Set credentials via environment variables:\n# access_key = \"...\" → AWS_ACCESS_KEY_ID\n# secret_key = \"...\" → AWS_SECRET_ACCESS_KEY\n\nskip_credentials_validation = true\nskip_metadata_api_check     = true\nskip_region_validation      = true\nskip_requesting_account_id  = true\nskip_s3_checksum            = true\nregion                      = \"auto\"\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/cloudflare/cloudflare\" {\n  version = \"5.14.0\"\n  hashes = [\n    \"h1:5rwgZxUA7qCU4HWcE7VUE5hPqrAH1Bk9Rr13qEeR1KY=\",\n    \"zh:0556cb6f38067c95e320f2d5680cf9851991d28448da903dd50b6c4b54de1c0b\",\n    \"zh:7cdd70418aa6571c27de4102e3cc3228f6edbe4a1eaa7927f772234ee09fb519\",\n    \"zh:84feef6c19993da06139e05dd6e1fceb7beb086f041cb9bc4edcae6081fe4812\",\n    \"zh:8b7dfcececcced324a8a8344fcb48b746f218174ffc691e36a54d09f2fb5e797\",\n    \"zh:99ba55ced06327bd8f16077f26d95593d3d77107e9faf204bf1a2485653eb02e\",\n    \"zh:9e93df24a28ffe7551458035bae8ea1f4fdab74a9a47ce61f1008bc18025ecd0\",\n    \"zh:a03e65a78c70578f3ebb3f2d84df516a33e2c3e9b62c0e3a2d2cea4f411c5e83\",\n    \"zh:e57ab18a3275dee18d69910a1103a52c8071d77e449e50b1a8b9d80779068145\",\n    \"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/google\" {\n  version     = \"7.12.0\"\n  constraints = \"~> 7.0\"\n  hashes = [\n    \"h1:kBKvDUp6GLwHAsoM6CIj9ZTxVBzSnQjyxaVSP8SfqHQ=\",\n    \"zh:38722ec7777543c23e22e02695e53dd5c94644022647c3c79e11e587063d4d2b\",\n    \"zh:417b12b69c91c12e3fcefee38744b7a37bae73b706e3071c714151a623a6b0e9\",\n    \"zh:4902cea92c78b462beaf053de03d0d55fb2241d41ca3379b4568ba247f667fa9\",\n    \"zh:50ccce39d403ba477943e6652ccb6913092d9dcce1d55533b00b66062888db3d\",\n    \"zh:56dccfe5df28cfe368d93c37ad6c46a16e76da61482fd0bfc83676b1423cecf5\",\n    \"zh:7265fca2921e5e300da5d8de7e28b658c0863fdda9da696c5b97dbd3122c17c2\",\n    \"zh:8317467e828178a6db9ddabe431bb13935c00bfb5e4b4d9760bd56f7ae596eca\",\n    \"zh:84cc9d9277422a0d6c80d2bd204642d8776ddbba23feb94cf2760bb5f15410bc\",\n    \"zh:8f79d72e7ed4e36d01560ce5fc944dc7e0387fa0f8272a4345fc6ae896e8f575\",\n    \"zh:98c3d756beca036f84e7840e2099ff7359e9a246cd9a35386e03ce65032b3f5f\",\n    \"zh:a07e3ca19673d28da9289ca28dfb83204fa6636f642b8cf46de8caaf526b7dde\",\n    \"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/random\" {\n  version = \"3.7.2\"\n  hashes = [\n    \"h1:KG4NuIBl1mRWU0KD/BGfCi1YN/j3F7H4YgeeM7iSdNs=\",\n    \"zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f\",\n    \"zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc\",\n    \"zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab\",\n    \"zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3\",\n    \"zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212\",\n    \"zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f\",\n    \"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3\",\n    \"zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34\",\n    \"zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967\",\n    \"zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d\",\n    \"zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62\",\n    \"zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0\",\n  ]\n}\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/README.md",
    "content": "# Hybrid Stack Root Template\n\nCopy this directory to enable hybrid stack for an environment:\n\n```bash\ncp -r infra/templates/env-roots/hybrid infra/envs/<env>/hybrid\n```\n\nAfter copying:\n\n1. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in values\n2. Never commit secrets—use `TF_VAR_*` environment variables in CI\n\n## Edge Routing (optional)\n\nTo add Cloudflare in front of Cloud Run, uncomment edge routing blocks in all three files:\n\n- `providers.tf` — Cloudflare provider\n- `variables.tf` — Cloudflare variables\n- `main.tf` — `enable_edge_routing = true` and related inputs\n\n> **Note:** Don't enable edge routing if you're also using the edge stack for the same hostname—they would conflict.\n\n## When to use hybrid\n\nOnly if you need Cloud Run compute, Vertex AI, or other GCP services. The edge stack handles most SaaS workloads.\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/main.tf",
    "content": "module \"stack\" {\n  source = \"../../../stacks/hybrid\"\n\n  gcp_project_id = var.gcp_project_id\n  gcp_region     = var.gcp_region\n  project_slug   = var.project_slug\n  environment    = var.environment\n  api_image      = var.api_image\n  cloud_sql_tier = var.cloud_sql_tier\n\n  # --- Edge routing (optional) ---\n  # enable_edge_routing = true\n  # cloudflare_zone_id  = var.cloudflare_zone_id\n  # hostname            = var.hostname\n}\n\noutput \"api_url\" {\n  value = module.stack.api_url\n}\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/providers.tf",
    "content": "terraform {\n  required_version = \">= 1.12, < 2.0\"\n\n  required_providers {\n    google = {\n      source  = \"hashicorp/google\"\n      version = \"~> 7.0\"\n    }\n  }\n}\n# See \"Provider Versions\" in docs/specs/infra-terraform.md\n\nprovider \"google\" {\n  project = var.gcp_project_id\n  region  = var.gcp_region\n}\n\n# --- Edge routing (optional) ---\n# Add these blocks to enable Cloudflare in front of Cloud Run.\n# Terraform initializes providers before planning, so credentials are required\n# even if no resources use them.\n#\n#   cloudflare = {\n#     source  = \"cloudflare/cloudflare\"\n#     version = \"~> 5.0\"\n#   }\n#\n# provider \"cloudflare\" {\n#   api_token = var.cloudflare_api_token\n# }\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/terraform.tfvars.example",
    "content": "# GCP Configuration\ngcp_project_id = \"my-gcp-project\"\ngcp_region     = \"us-central1\"\n\n# Project Configuration\nproject_slug = \"myapp\"\nenvironment  = \"prod\"\n\n# API Configuration\napi_image      = \"gcr.io/my-gcp-project/api:latest\"\ncloud_sql_tier = \"db-f1-micro\"\n\n# --- Edge routing (optional) ---\n# cloudflare_account_id = \"your-account-id\"\n# cloudflare_zone_id    = \"your-zone-id\"\n# hostname              = \"api.example.com\"\n# TF_VAR_cloudflare_api_token: set via environment variable in CI\n"
  },
  {
    "path": "infra/templates/env-roots/hybrid/variables.tf",
    "content": "variable \"gcp_project_id\" {\n  type = string\n}\n\nvariable \"gcp_region\" {\n  type = string\n}\n\nvariable \"project_slug\" {\n  type        = string\n  description = \"Short identifier for resource naming (e.g., myapp)\"\n}\n\nvariable \"environment\" {\n  type        = string\n  description = \"Environment name (e.g., dev, staging, prod)\"\n}\n\nvariable \"api_image\" {\n  type = string\n}\n\nvariable \"cloud_sql_tier\" {\n  type        = string\n  description = \"Cloud SQL instance tier (e.g., db-f1-micro)\"\n  default     = \"db-f1-micro\"\n}\n\n# --- Edge routing (optional) ---\n# Uncomment to add Cloudflare edge layer in front of Cloud Run.\n# Also uncomment the Cloudflare provider in providers.tf and module inputs in main.tf.\n#\n# variable \"cloudflare_api_token\" {\n#   type      = string\n#   sensitive = true\n# }\n#\n# variable \"cloudflare_zone_id\" {\n#   type    = string\n#   default = \"\"\n# }\n#\n# variable \"hostname\" {\n#   type = string\n# }\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@repo/root\",\n  \"version\": \"0.0.0\",\n  \"packageManager\": \"bun@1.3.9\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"engines\": {\n    \"bun\": \">=1.3.0\"\n  },\n  \"workspaces\": [\n    \"apps/*\",\n    \"db\",\n    \"packages/*\",\n    \"scripts\"\n  ],\n  \"scripts\": {\n    \"dev\": \"bun --filter @repo/web --filter @repo/api --filter @repo/app dev\",\n    \"lint\": \"eslint --cache --report-unused-disable-directives .\",\n    \"test\": \"vitest\",\n    \"typecheck\": \"tsc --build\",\n    \"build:types\": \"tsc --build\",\n    \"build\": \"bun --filter @repo/email --filter @repo/web --filter @repo/api --filter @repo/app build\",\n    \"generate:types\": \"bun wrangler types --config apps/api/wrangler.jsonc --include-runtime=false apps/api/types/cloudflare-env.d.ts && bun prettier --write apps/api/types/*-env.d.ts\",\n    \"prepare\": \"husky\",\n    \"docs:dev\": \"vitepress dev docs\",\n    \"docs:build\": \"vitepress build docs\",\n    \"docs:preview\": \"vitepress preview docs\",\n    \"docs:deploy\": \"gh-pages --dist docs/.vitepress/dist\",\n    \"app:dev\": \"bun --cwd apps/app dev\",\n    \"app:build\": \"bun --cwd apps/app build\",\n    \"app:test\": \"bun --cwd apps/app test\",\n    \"app:deploy\": \"bun --cwd apps/app deploy\",\n    \"web:dev\": \"bun --cwd apps/web dev\",\n    \"web:build\": \"bun --cwd apps/web build\",\n    \"web:test\": \"bun --cwd apps/web test\",\n    \"web:deploy\": \"bun --cwd apps/web deploy\",\n    \"api:dev\": \"bun --cwd apps/api dev\",\n    \"api:build\": \"bun --cwd apps/api build\",\n    \"api:build:docker\": \"docker build --tag api:latest -f ./apps/api/Dockerfile .\",\n    \"api:test\": \"bun --cwd apps/api test\",\n    \"api:deploy\": \"bun --cwd apps/api deploy\",\n    \"ui:add\": \"bun --cwd packages/ui add\",\n    \"ui:list\": \"bun --cwd packages/ui list\",\n    \"ui:update\": \"bun --cwd packages/ui update\",\n    \"ui:essentials\": \"bun --cwd packages/ui essentials\",\n    \"email:dev\": \"bun --cwd apps/email dev\",\n    \"email:build\": \"bun --cwd apps/email build\",\n    \"email:export\": \"bun --cwd apps/email export\",\n    \"db:generate\": \"bun --cwd db generate\",\n    \"db:generate:staging\": \"bun --cwd db generate:staging\",\n    \"db:generate:prod\": \"bun --cwd db generate:prod\",\n    \"db:migrate\": \"bun --cwd db migrate\",\n    \"db:migrate:staging\": \"bun --cwd db migrate:staging\",\n    \"db:migrate:prod\": \"bun --cwd db migrate:prod\",\n    \"db:push\": \"bun --cwd db push\",\n    \"db:push:staging\": \"bun --cwd db push:staging\",\n    \"db:push:prod\": \"bun --cwd db push:prod\",\n    \"db:studio\": \"bun --cwd db studio\",\n    \"db:studio:staging\": \"bun --cwd db studio:staging\",\n    \"db:studio:prod\": \"bun --cwd db studio:prod\",\n    \"db:seed\": \"bun --cwd db seed\",\n    \"db:seed:staging\": \"bun --cwd db seed:staging\",\n    \"db:seed:prod\": \"bun --cwd db seed:prod\",\n    \"db:export\": \"bun --cwd db export\",\n    \"db:export:staging\": \"bun --cwd db export:staging\",\n    \"db:export:prod\": \"bun --cwd db export:prod\",\n    \"db:check\": \"bun --cwd db check\",\n    \"db:typecheck\": \"bun --cwd db typecheck\",\n    \"infra:dev\": \"bun run infra:dev:edge:apply\",\n    \"infra:dev:edge:plan\": \"terraform -chdir=infra/envs/dev/edge plan\",\n    \"infra:dev:edge:apply\": \"terraform -chdir=infra/envs/dev/edge apply -auto-approve\",\n    \"infra:dev:edge:destroy\": \"terraform -chdir=infra/envs/dev/edge destroy -auto-approve\",\n    \"infra:preview\": \"bun run infra:preview:edge:apply\",\n    \"infra:preview:edge:plan\": \"terraform -chdir=infra/envs/preview/edge plan\",\n    \"infra:preview:edge:apply\": \"terraform -chdir=infra/envs/preview/edge apply -auto-approve\",\n    \"infra:preview:edge:destroy\": \"terraform -chdir=infra/envs/preview/edge destroy -auto-approve\",\n    \"infra:staging\": \"bun run infra:staging:edge:apply\",\n    \"infra:staging:edge:plan\": \"terraform -chdir=infra/envs/staging/edge plan\",\n    \"infra:staging:edge:apply\": \"terraform -chdir=infra/envs/staging/edge apply\",\n    \"infra:staging:edge:destroy\": \"terraform -chdir=infra/envs/staging/edge destroy\",\n    \"infra:prod\": \"bun run infra:prod:edge:apply\",\n    \"infra:prod:edge:plan\": \"terraform -chdir=infra/envs/prod/edge plan\",\n    \"infra:prod:edge:apply\": \"terraform -chdir=infra/envs/prod/edge apply\",\n    \"infra:prod:edge:destroy\": \"terraform -chdir=infra/envs/prod/edge destroy\"\n  },\n  \"devDependencies\": {\n    \"@emotion/babel-plugin\": \"^11.13.5\",\n    \"@emotion/eslint-plugin\": \"^11.12.0\",\n    \"@eslint-react/eslint-plugin\": \"^2.12.4\",\n    \"@eslint/js\": \"^9.0.0\",\n    \"@types/eslint\": \"^9.6.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.55.0\",\n    \"@typescript-eslint/parser\": \"^8.55.0\",\n    \"@ws-kit/zod\": \"^0.10.2\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-import-resolver-typescript\": \"^4.4.4\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.10.2\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"gh-pages\": \"^6.3.0\",\n    \"globals\": \"^17.3.0\",\n    \"graphql\": \"^16.12.0\",\n    \"happy-dom\": \"^20.6.2\",\n    \"husky\": \"^9.1.7\",\n    \"jiti\": \"^2.6.1\",\n    \"lint-staged\": \"^16.2.7\",\n    \"mermaid\": \"^11.12.3\",\n    \"npm-check\": \"^6.0.1\",\n    \"prettier\": \"^3.8.1\",\n    \"react\": \"^19.2.4\",\n    \"relay-config\": \"^12.0.1\",\n    \"srcpack\": \"^0.1.15\",\n    \"typescript\": \"~5.9.3\",\n    \"typescript-eslint\": \"^8.56.0\",\n    \"typescript-language-server\": \"^5.1.3\",\n    \"vite\": \"~7.3.1\",\n    \"vitepress\": \"2.0.0-alpha.16\",\n    \"vitepress-plugin-llms\": \"^1.11.0\",\n    \"vitest\": \"~4.0.18\",\n    \"wrangler\": \"^4.66.0\"\n  },\n  \"prettier\": {\n    \"printWidth\": 80,\n    \"tabWidth\": 2,\n    \"useTabs\": false,\n    \"semi\": true,\n    \"singleQuote\": false,\n    \"quoteProps\": \"as-needed\",\n    \"jsxSingleQuote\": false,\n    \"trailingComma\": \"all\",\n    \"bracketSpacing\": true,\n    \"bracketSameLine\": false,\n    \"arrowParens\": \"always\",\n    \"endOfLine\": \"lf\",\n    \"overrides\": [\n      {\n        \"files\": \"*.jsonc\",\n        \"options\": {\n          \"trailingComma\": \"none\"\n        }\n      }\n    ]\n  },\n  \"lint-staged\": {\n    \"*.{js,jsx,ts,tsx,mjs,cjs}\": [\n      \"bunx eslint --max-warnings=0 --report-unused-disable-directives\"\n    ],\n    \"*\": [\n      \"bunx prettier --check --ignore-unknown\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# Core Package\n\nShared utilities and helpers used across the monorepo.\n"
  },
  {
    "path": "packages/core/index.ts",
    "content": "/**\n * @file Core package entrypoint.\n *\n * Placeholder for shared utilities and WebSocket functionality.\n */\n\nexport default {};\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@repo/core\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./index.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/bun\": \"^1.3.9\",\n    \"typescript\": \"~5.9.3\"\n  }\n}\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"extends\": \"../typescript-config/node.jsonc\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./\"\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"**/node_modules/**/*\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/typescript-config/README.md",
    "content": "# TypeScript Configuration\n\nShared TypeScript configuration for the monorepo.\n\n## Usage\n\nExtend from the appropriate configuration in your `tsconfig.json`:\n\n```jsonc\n// React applications\n{ \"extends\": \"@repo/typescript-config/react.jsonc\" }\n\n// Node.js/Bun applications\n{ \"extends\": \"@repo/typescript-config/node.jsonc\" }\n\n// Cloudflare Workers\n{ \"extends\": \"@repo/typescript-config/cloudflare.jsonc\" }\n```\n\n## Available Configurations\n\n- `base.jsonc` -- Core strict-mode configuration shared by all targets\n- `react.jsonc` -- React applications with DOM types and JSX support\n- `node.jsonc` -- Node.js/Bun backend services\n- `cloudflare.jsonc` -- Cloudflare Workers edge functions\n"
  },
  {
    "path": "packages/typescript-config/base.jsonc",
    "content": "{\n  /* Visit https://aka.ms/tsconfig to read more about this file */\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    /* Type Checking */\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitOverride\": true,\n    \"noImplicitThis\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": true,\n\n    /* Modules */\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"baseUrl\": \"../../\",\n    \"paths\": {\n      \"@repo/api\": [\"apps/api\"],\n      \"@repo/api/*\": [\"apps/api/*\"],\n      \"@repo/core\": [\"packages/core\"],\n      \"@repo/core/*\": [\"packages/core/*\"],\n      \"@repo/db\": [\"db\"],\n      \"@repo/db/*\": [\"db/*\"],\n      \"@repo/ws-protocol\": [\"packages/ws-protocol\"],\n      \"@repo/ws-protocol/*\": [\"packages/ws-protocol/*\"],\n      \"@/*\": [\"apps/web/*\"]\n    },\n    \"resolveJsonModule\": true,\n    \"allowImportingTsExtensions\": false,\n\n    /* Emit */\n    // noEmit is set per-package based on needs\n\n    /* JavaScript Support */\n    \"allowJs\": true,\n\n    /* Interop Constraints */\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n\n    /* Language and Environment */\n    \"target\": \"ESNext\",\n    \"lib\": [\"ESNext\"],\n    \"useDefineForClassFields\": true,\n\n    /* Completeness */\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/cloudflare.jsonc",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.jsonc\",\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\"],\n    \"types\": [\"@cloudflare/workers-types\"]\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/node.jsonc",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.jsonc\",\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\"],\n    \"types\": [\"bun\"]\n  }\n}\n"
  },
  {
    "path": "packages/typescript-config/package.json",
    "content": "{\n  \"name\": \"@repo/typescript-config\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"exports\": {\n    \"./base\": \"./base.jsonc\",\n    \"./base.jsonc\": \"./base.jsonc\",\n    \"./cloudflare\": \"./cloudflare.jsonc\",\n    \"./cloudflare.jsonc\": \"./cloudflare.jsonc\",\n    \"./node\": \"./node.jsonc\",\n    \"./node.jsonc\": \"./node.jsonc\",\n    \"./react\": \"./react.jsonc\",\n    \"./react.jsonc\": \"./react.jsonc\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"files\": [\n    \"*.jsonc\",\n    \"package.json\"\n  ]\n}\n"
  },
  {
    "path": "packages/typescript-config/react.jsonc",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"./base.jsonc\",\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"types\": [\"vite/client\"]\n  }\n}\n"
  },
  {
    "path": "packages/ui/README.md",
    "content": "# UI Components\n\nShared UI component library built on shadcn/ui (new-york style), Radix UI, and Tailwind CSS v4.\n\n[Documentation](https://reactstarter.com/frontend/ui)\n\n## Usage\n\n```typescript\nimport { Button, Card, Input, cn } from \"@repo/ui\";\n```\n\n## Commands\n\n```bash\nbun ui:add <component>    # Add a shadcn/ui component\nbun ui:list               # List installed components\nbun ui:essentials         # Install curated essential set\n```\n\n## Structure\n\n```bash\ncomponents/       # shadcn/ui components\nhooks/            # Custom React hooks\nlib/              # Utilities (cn function)\nscripts/          # Component management tools\n```\n\nConsuming apps must include `@source \"../../packages/ui/components/**/*.{ts,tsx}\"` in their Tailwind config.\n"
  },
  {
    "path": "packages/ui/components/avatar.tsx",
    "content": "import * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "packages/ui/components/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "packages/ui/components/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-xl border bg-card text-card-foreground shadow\",\n      className,\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "packages/ui/components/checkbox.tsx",
    "content": "import * as React from \"react\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { Check } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className,\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn(\"grid place-content-center text-current\")}\n    >\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n));\nCheckbox.displayName = CheckboxPrimitive.Root.displayName;\n\nexport { Checkbox };\n"
  },
  {
    "path": "packages/ui/components/dialog.tsx",
    "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "packages/ui/components/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "packages/ui/components/label.tsx",
    "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "packages/ui/components/radio-group.tsx",
    "content": "import * as React from \"react\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-3.5 w-3.5 fill-primary\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "packages/ui/components/scroll-area.tsx",
    "content": "import * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "packages/ui/components/select.tsx",
    "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "packages/ui/components/separator.tsx",
    "content": "import * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref,\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "packages/ui/components/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-primary/10\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "packages/ui/components/switch.tsx",
    "content": "import * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "packages/ui/components/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Textarea = React.forwardRef<\n  HTMLTextAreaElement,\n  React.ComponentProps<\"textarea\">\n>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  );\n});\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "packages/ui/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"css\": \"styles.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "packages/ui/hooks/index.ts",
    "content": "// Export custom hooks here when they are added\n"
  },
  {
    "path": "packages/ui/index.ts",
    "content": "/**\n * @file UI component library entrypoint.\n *\n * Re-exports all shadcn/ui components, utilities, and hooks.\n */\n\nexport * from \"./components/avatar\";\nexport * from \"./components/button\";\nexport * from \"./components/card\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/dialog\";\nexport * from \"./components/input\";\nexport * from \"./components/label\";\nexport * from \"./components/radio-group\";\nexport * from \"./components/scroll-area\";\nexport * from \"./components/select\";\nexport * from \"./components/separator\";\nexport * from \"./components/skeleton\";\nexport * from \"./components/switch\";\nexport * from \"./components/textarea\";\n\n// Export utilities\nexport * from \"./lib/utils\";\n\n// Export hooks\n"
  },
  {
    "path": "packages/ui/lib/utils.ts",
    "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "packages/ui/package.json",
    "content": "{\n  \"name\": \"@repo/ui\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Reusable UI components for React Starter Kit (monorepo)\",\n  \"type\": \"module\",\n  \"main\": \"./index.ts\",\n  \"types\": \"./index.ts\",\n  \"exports\": {\n    \".\": \"./index.ts\",\n    \"./lib/utils\": \"./lib/utils.ts\",\n    \"./components/*\": \"./components/*.tsx\",\n    \"./hooks/*\": \"./hooks/*.ts\",\n    \"./lib/*\": \"./lib/*.ts\"\n  },\n  \"files\": [\n    \"components\",\n    \"hooks\",\n    \"lib\",\n    \"index.ts\",\n    \"components.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"dev\": \"tsc --watch\",\n    \"lint\": \"eslint . --max-warnings 0\",\n    \"type-check\": \"tsc --noEmit\",\n    \"add\": \"bun scripts/add.ts\",\n    \"list\": \"bun scripts/list.ts\",\n    \"update\": \"bun scripts/update.ts\",\n    \"essentials\": \"bun scripts/essentials.ts\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=19.2.4\",\n    \"react-dom\": \">=19.2.4\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-avatar\": \"^1.1.11\",\n    \"@radix-ui/react-checkbox\": \"^1.3.3\",\n    \"@radix-ui/react-dialog\": \"^1.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.8\",\n    \"@radix-ui/react-radio-group\": \"^1.3.8\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n    \"@radix-ui/react-select\": \"^2.2.6\",\n    \"@radix-ui/react-separator\": \"^1.1.8\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.6\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"^0.574.0\",\n    \"tailwind-merge\": \"^3.4.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react-dom\": \"^19.2.3\",\n    \"@types/react\": \"^19.2.14\",\n    \"tailwindcss\": \"^4.2.0\",\n    \"typescript\": \"~5.9.3\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "packages/ui/scripts/add.ts",
    "content": "#!/usr/bin/env bun\nimport { execCommand, formatGeneratedFiles } from \"./format-utils.js\";\n\nasync function addComponent(): Promise<void> {\n  const args = process.argv.slice(2);\n\n  if (args.length === 0 || args.includes(\"--help\") || args.includes(\"-h\")) {\n    console.log(\"📋 shadcn/ui Component Installer\");\n    console.log(\"=================================\\n\");\n    console.log(\"Usage:\");\n    console.log(\"  bun run ui:add <component>     Add a single component\");\n    console.log(\"  bun run ui:add <comp1> <comp2> Add multiple components\");\n    console.log(\n      \"  bun run ui:add --all           Add all available components\",\n    );\n    console.log(\"\\nExamples:\");\n    console.log(\"  bun run ui:add button\");\n    console.log(\"  bun run ui:add button card input\");\n    console.log(\"  bun run ui:add dialog alert-dialog toast\");\n\n    if (args.length === 0) {\n      process.exit(1);\n    } else {\n      process.exit(0);\n    }\n  }\n\n  console.log(\"🚀 Adding shadcn/ui components...\");\n\n  try {\n    const shadcnArgs = [\"shadcn@latest\", \"add\", ...args, \"--yes\"];\n\n    console.log(`Running: bunx ${shadcnArgs.join(\" \")}`);\n    await execCommand(\"bunx\", shadcnArgs);\n\n    await formatGeneratedFiles();\n\n    console.log(\"✅ Components added successfully!\");\n  } catch (error) {\n    console.error(\"❌ Failed to add components:\", error);\n    process.exit(1);\n  }\n}\n\naddComponent().catch(console.error);\n"
  },
  {
    "path": "packages/ui/scripts/essentials.ts",
    "content": "#!/usr/bin/env bun\nimport { execCommand, formatGeneratedFiles } from \"./format-utils.js\";\n\n// Essential components for most applications\nconst ESSENTIAL_COMPONENTS = [\n  // Forms & Inputs\n  \"button\",\n  \"input\",\n  \"textarea\",\n  \"select\",\n  \"checkbox\",\n  \"radio-group\",\n  \"switch\",\n  \"label\",\n  \"form\",\n  // Layout & Structure\n  \"card\",\n  \"separator\",\n  \"skeleton\",\n  \"scroll-area\",\n  // Navigation\n  \"navigation-menu\",\n  \"breadcrumb\",\n  \"tabs\",\n  // Feedback & Communication\n  \"dialog\",\n  \"alert-dialog\",\n  \"toast\",\n  \"alert\",\n  \"badge\",\n  \"progress\",\n  // Data Display\n  \"avatar\",\n  \"tooltip\",\n  \"popover\",\n];\n\nasync function installEssentials(): Promise<void> {\n  const args = process.argv.slice(2);\n\n  if (args.includes(\"--help\") || args.includes(\"-h\")) {\n    console.log(\"🎯 shadcn/ui Essential Components Installer\");\n    console.log(\"===========================================\\n\");\n    console.log(\n      \"Installs a curated set of essential shadcn/ui components for most applications.\\n\",\n    );\n    console.log(\"Essential components include:\");\n    console.log(\n      \"• Forms: button, input, textarea, select, checkbox, radio-group, switch, label, form\",\n    );\n    console.log(\"• Layout: card, separator, skeleton, scroll-area\");\n    console.log(\"• Navigation: navigation-menu, breadcrumb, tabs\");\n    console.log(\n      \"• Feedback: dialog, alert-dialog, toast, alert, badge, progress\",\n    );\n    console.log(\"• Data Display: avatar, tooltip, popover\\n\");\n    console.log(\"Usage:\");\n    console.log(\n      \"  bun run ui:essentials          Install all essential components\",\n    );\n    console.log(\n      \"  bun run ui:essentials --list   List essential components without installing\",\n    );\n    process.exit(0);\n  }\n\n  if (args.includes(\"--list\")) {\n    console.log(\"📋 Essential shadcn/ui Components\");\n    console.log(\"==================================\\n\");\n    console.log(`Total: ${ESSENTIAL_COMPONENTS.length} components\\n`);\n\n    ESSENTIAL_COMPONENTS.forEach((component, index) => {\n      console.log(`${(index + 1).toString().padStart(2)}. ${component}`);\n    });\n\n    console.log(\"\\n💡 To install these components, run:\");\n    console.log(\"  bun run ui:essentials\");\n    return;\n  }\n\n  console.log(\"🎯 Installing Essential shadcn/ui Components\");\n  console.log(\"=============================================\\n\");\n  console.log(\n    `Installing ${ESSENTIAL_COMPONENTS.length} essential components...\\n`,\n  );\n\n  try {\n    const shadcnArgs = [\n      \"shadcn@latest\",\n      \"add\",\n      ...ESSENTIAL_COMPONENTS,\n      \"--yes\",\n    ];\n\n    await execCommand(\"bunx\", shadcnArgs);\n\n    await formatGeneratedFiles();\n\n    console.log(\"\\n🎉 Essential components installed successfully!\");\n    console.log(\"\\n📊 Summary:\");\n    console.log(`✅ Installed ${ESSENTIAL_COMPONENTS.length} components`);\n    console.log(\"\\n💡 View installed components with:\");\n    console.log(\"  bun run ui:list\");\n  } catch (error) {\n    console.error(\"❌ Failed to install essential components:\", error);\n    process.exit(1);\n  }\n}\n\ninstallEssentials().catch(console.error);\n"
  },
  {
    "path": "packages/ui/scripts/format-utils.ts",
    "content": "#!/usr/bin/env bun\nimport { join } from \"node:path\";\nimport { Glob } from \"bun\";\n\n/**\n * Execute a command with inherited stdio\n */\nexport async function execCommand(\n  command: string,\n  args: string[],\n): Promise<void> {\n  const proc = Bun.spawn([command, ...args], {\n    stdio: [\"inherit\", \"inherit\", \"inherit\"],\n  });\n\n  const exitCode = await proc.exited;\n  if (exitCode !== 0) {\n    throw new Error(`Command failed: ${command} ${args.join(\" \")}`);\n  }\n}\n\n/**\n * Format generated UI component files with Prettier\n */\nexport async function formatGeneratedFiles(): Promise<void> {\n  try {\n    const componentsDir = join(import.meta.dirname, \"../components\");\n\n    const glob = new Glob(\"**/*.{ts,tsx}\");\n    const componentFiles: string[] = [];\n\n    for await (const file of glob.scan({\n      cwd: componentsDir,\n      absolute: true,\n    })) {\n      componentFiles.push(file);\n    }\n\n    if (componentFiles.length === 0) {\n      return;\n    }\n\n    console.log(\"🎨 Formatting generated files with Prettier...\");\n\n    await execCommand(\"bunx\", [\"prettier\", \"--write\", ...componentFiles]);\n\n    console.log(\"✨ Files formatted successfully\");\n  } catch (error) {\n    console.warn(\"⚠️  Failed to format files with Prettier:\", error);\n  }\n}\n"
  },
  {
    "path": "packages/ui/scripts/list.ts",
    "content": "#!/usr/bin/env bun\nimport { existsSync } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\n\ninterface ComponentInfo {\n  name: string;\n  size: number;\n  modified: Date;\n}\n\nasync function getComponentInfo(filePath: string): Promise<ComponentInfo> {\n  const stats = await stat(filePath);\n  const name = basename(filePath, \".tsx\");\n\n  return {\n    name,\n    size: stats.size,\n    modified: stats.mtime,\n  };\n}\n\nasync function listComponents(): Promise<void> {\n  console.log(\"📋 shadcn/ui Component Inventory\");\n  console.log(\"=============================\\n\");\n\n  const componentsDir = join(import.meta.dirname, \"../components\");\n\n  if (!existsSync(componentsDir)) {\n    console.log(`❌ Components directory not found: ${componentsDir}`);\n    console.log(\"💡 Run 'bunx shadcn@latest init' to set up shadcn/ui first\");\n    process.exit(1);\n  }\n\n  try {\n    const files = await readdir(componentsDir);\n    const tsxFiles = files.filter((file) => file.endsWith(\".tsx\"));\n\n    if (tsxFiles.length === 0) {\n      console.log(\"❌ No shadcn/ui components found\");\n      console.log(\"💡 Add components with: bun run ui:add <component-name>\");\n      return;\n    }\n\n    console.log(\"📦 Installed Components:\\n\");\n\n    const components: ComponentInfo[] = [];\n\n    for (const file of tsxFiles) {\n      const filePath = join(componentsDir, file);\n      const info = await getComponentInfo(filePath);\n      components.push(info);\n    }\n\n    // Sort by name\n    components.sort((a, b) => a.name.localeCompare(b.name));\n\n    // Display components\n    for (const component of components) {\n      const formattedSize = component.size.toLocaleString();\n      const formattedDate = component.modified\n        .toISOString()\n        .slice(0, 16)\n        .replace(\"T\", \" \");\n      console.log(\n        `• ${component.name.padEnd(20)} ${formattedSize.padStart(8)} bytes  ${formattedDate}`,\n      );\n    }\n\n    console.log(\"\\n📊 Summary:\");\n    console.log(`Total components: ${components.length}`);\n\n    console.log(\"\\n🔄 To update all components, run:\");\n    console.log(\"  bun run ui:update\");\n  } catch (error) {\n    console.error(\"Error reading components:\", error);\n    process.exit(1);\n  }\n}\n\nlistComponents().catch(console.error);\n"
  },
  {
    "path": "packages/ui/scripts/update.ts",
    "content": "#!/usr/bin/env bun\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport { execCommand, formatGeneratedFiles } from \"./format-utils.js\";\n\nasync function getInstalledComponents(): Promise<string[]> {\n  const componentsDir = join(import.meta.dirname, \"../components\");\n\n  if (!existsSync(componentsDir)) {\n    throw new Error(`Components directory not found: ${componentsDir}`);\n  }\n\n  const files = await readdir(componentsDir);\n  const tsxFiles = files.filter((file) => file.endsWith(\".tsx\"));\n\n  return tsxFiles.map((file) => basename(file, \".tsx\"));\n}\n\nasync function updateComponents(): Promise<void> {\n  console.log(\"🔍 Finding installed shadcn/ui components...\");\n\n  try {\n    const components = await getInstalledComponents();\n\n    if (components.length === 0) {\n      console.log(\"❌ No shadcn/ui components found in packages/ui/components\");\n      process.exit(1);\n    }\n\n    console.log(`📦 Found ${components.length} components:`);\n    components.forEach((component) => console.log(`  • ${component}`));\n\n    console.log(\"\\n🚀 Updating components...\");\n\n    for (const component of components) {\n      console.log(`\\n⏳ Updating ${component}...`);\n\n      try {\n        await execCommand(\"bunx\", [\n          \"shadcn@latest\",\n          \"add\",\n          component,\n          \"--overwrite\",\n          \"--yes\",\n        ]);\n        console.log(`✅ ${component} updated successfully`);\n      } catch (error) {\n        console.error(`❌ Failed to update ${component}:`, error);\n      }\n    }\n\n    await formatGeneratedFiles();\n\n    console.log(\"\\n🎉 All components update process completed!\");\n  } catch (error) {\n    console.error(\"Error updating components:\", error);\n    process.exit(1);\n  }\n}\n\nasync function updateSpecificComponent(component: string): Promise<void> {\n  console.log(`🚀 Updating specific component: ${component}...`);\n  try {\n    await execCommand(\"bunx\", [\n      \"shadcn@latest\",\n      \"add\",\n      component,\n      \"--overwrite\",\n      \"--yes\",\n    ]);\n    await formatGeneratedFiles();\n    console.log(`✅ ${component} updated successfully`);\n  } catch (error) {\n    console.error(`❌ Failed to update ${component}:`, error);\n    process.exit(1);\n  }\n}\n\nasync function main(): Promise<void> {\n  const args = process.argv.slice(2);\n\n  if (args.includes(\"--help\") || args.includes(\"-h\")) {\n    console.log(\"🔄 shadcn/ui Component Updater\");\n    console.log(\"===============================\\n\");\n    console.log(\"Usage:\");\n    console.log(\n      \"  bun run ui:update              Update all installed components\",\n    );\n    console.log(\"  bun run ui:update <component>  Update a specific component\");\n    console.log(\"\\nExamples:\");\n    console.log(\"  bun run ui:update\");\n    console.log(\"  bun run ui:update button\");\n    process.exit(0);\n  }\n\n  if (args.length > 0) {\n    await updateSpecificComponent(args[0]);\n  } else {\n    await updateComponents();\n  }\n}\n\nmain().catch(console.error);\n"
  },
  {
    "path": "packages/ui/styles.css",
    "content": "/* shadcn CLI configuration only - actual styles in app globals.css */\n\n@import \"tailwindcss/preflight\";\n@import \"tailwindcss/utilities\";\n"
  },
  {
    "path": "packages/ui/tsconfig.json",
    "content": "{\n  \"extends\": \"../typescript-config/react.jsonc\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./\",\n    \"tsBuildInfoFile\": \"../../.cache/tsconfig/ui.tsbuildinfo\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"components\", \"hooks\", \"lib\", \"index.ts\"],\n  \"exclude\": [\"dist\", \"node_modules\"]\n}\n"
  },
  {
    "path": "packages/ws-protocol/README.md",
    "content": "# WebSocket Protocol\n\nType-safe WebSocket protocol definitions using [WS-Kit](https://github.com/kriasoft/ws-kit).\n\n[Documentation](https://reactstarter.com/recipes/websockets)\n\n## Quick Start\n\n```bash\nbun run example       # Run example server\n```\n\n## Usage\n\n```typescript\nimport { z, message, rpc } from \"@ws-kit/zod\";\nimport { createRouter, withZod } from \"@ws-kit/zod\";\n\nconst Ping = message(\"PING\", { timestamp: z.number().optional() });\nconst Pong = message(\"PONG\", { timestamp: z.number() });\n\nconst router = createRouter<{ userId?: string }>()\n  .plugin(withZod())\n  .on(Ping, (ctx) => {\n    ctx.send(Pong, { timestamp: Date.now() });\n  });\n```\n\n## Structure\n\n```bash\nmessages.ts   # Message schema definitions\nrouter.ts     # Router factory with handlers\nexample.ts    # Example server\nindex.ts      # Public exports\n```\n"
  },
  {
    "path": "packages/ws-protocol/example.ts",
    "content": "/**\n * Minimal WebSocket server example using WS-Kit.\n *\n * Run with: bun run example.ts\n */\n\nimport { createBunHandler } from \"@ws-kit/bun\";\nimport { memoryPubSub } from \"@ws-kit/memory\";\nimport { withPubSub } from \"@ws-kit/pubsub\";\nimport { createAppRouter, Notification } from \"./index\";\n\n// Create the router with pub/sub support\nconst router = createAppRouter().plugin(\n  withPubSub({ adapter: memoryPubSub() }),\n);\n\n// Create Bun WebSocket handlers\nconst { fetch: handleWebSocket, websocket } = createBunHandler(router, {\n  authenticate() {\n    return { connectedAt: Date.now() };\n  },\n});\n\nconst server = Bun.serve({\n  port: 3000,\n\n  fetch(req, server) {\n    const url = new URL(req.url);\n\n    if (url.pathname === \"/ws\") {\n      return handleWebSocket(req, server);\n    }\n\n    // Broadcast endpoint for testing pub/sub\n    if (url.pathname === \"/broadcast\" && req.method === \"POST\") {\n      router.publish(\"notifications\", Notification, {\n        level: \"info\",\n        message: \"Hello to all connected clients!\",\n      });\n      return new Response(\"Broadcast sent\");\n    }\n\n    return new Response(\n      `\nWebSocket Server Example\n\nConnect to ws://localhost:3000/ws\n\nTry sending:\n- {\"type\": \"PING\", \"meta\": {}, \"payload\": {}}\n- {\"type\": \"ECHO\", \"meta\": {}, \"payload\": {\"text\": \"Hello\"}}\n- {\"type\": \"GET_USER\", \"meta\": {\"correlationId\": \"1\"}, \"payload\": {\"id\": \"123\"}}\n\nBroadcast to all clients:\n- curl -X POST http://localhost:3000/broadcast\n    `,\n      {\n        headers: { \"content-type\": \"text/plain\" },\n      },\n    );\n  },\n\n  websocket,\n});\n\nconsole.log(`\nWebSocket server running at:\n   HTTP: http://localhost:${server.port}\n   WebSocket: ws://localhost:${server.port}/ws\n\nTest with:\n   wscat -c ws://localhost:${server.port}/ws\n`);\n\n// ============================================================================\n// Example Client Code (for reference)\n// ============================================================================\n/*\nimport { wsClient, message, z } from \"@ws-kit/client/zod\";\nimport { Ping, Pong, Echo, GetUser } from \"@repo/ws-protocol/messages\";\n\n// Create typed client\nconst client = wsClient({\n  url: \"ws://localhost:3000/ws\",\n  reconnect: { enabled: true },\n});\n\n// Handle incoming messages\nclient.on(Pong, (msg) => {\n  console.log(\"Received Pong\");\n});\n\nclient.on(Echo, (msg) => {\n  console.log(\"Echo response:\", msg.payload.text);\n});\n\n// Connect and send messages\nawait client.connect();\n\n// Send ping\nclient.send(Ping);\n\n// Send echo\nclient.send(Echo, { text: \"Hello server!\" });\n\n// RPC request with typed response\nconst user = await client.request(\n  GetUser,\n  { id: \"123\" },\n  GetUser.response,\n  { timeoutMs: 5000 }\n);\nconsole.log(\"User:\", user.payload.name);\n\n// Cleanup\nawait client.close();\n*/\n"
  },
  {
    "path": "packages/ws-protocol/index.ts",
    "content": "/**\n * WebSocket protocol definitions for the application.\n *\n * @example\n * ```ts\n * import { createAppRouter, Ping, Pong, Echo } from \"@repo/ws-protocol\";\n *\n * const router = createAppRouter();\n * ```\n */\n\nexport * from \"./messages\";\nexport * from \"./router\";\n"
  },
  {
    "path": "packages/ws-protocol/messages.ts",
    "content": "/**\n * WebSocket message schemas for the application protocol.\n *\n * Uses @ws-kit/zod for type-safe message definitions with Zod validation.\n * All messages follow the envelope structure: { type, meta, payload }.\n *\n * @example\n * ```ts\n * import { Ping, Pong, Echo } from \"@repo/ws-protocol\";\n *\n * // Send a ping\n * ctx.send(Ping);\n *\n * // Send an echo with payload\n * ctx.send(Echo, { text: \"Hello\" });\n * ```\n */\n\nimport { message, rpc, z } from \"@ws-kit/zod\";\n\n// ============================================================================\n// Connection Health\n// ============================================================================\n\n/** Ping message for connection health checks. Server responds with Pong. */\nexport const Ping = message(\"PING\", { timestamp: z.number().optional() });\n\n/** Pong message sent in response to Ping. */\nexport const Pong = message(\"PONG\", { timestamp: z.number().optional() });\n\n// ============================================================================\n// Echo (Simple Request/Response)\n// ============================================================================\n\n/** Echo message - server echoes back the same text. */\nexport const Echo = message(\"ECHO\", { text: z.string() });\n\n// ============================================================================\n// Notifications\n// ============================================================================\n\n/** Server notification broadcast to clients. */\nexport const Notification = message(\"NOTIFICATION\", {\n  level: z.enum([\"info\", \"warning\", \"error\"]),\n  message: z.string(),\n});\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\n/** Error message for communicating protocol-level errors. */\nexport const ErrorMessage = message(\"ERROR\", {\n  code: z.enum([\"INVALID_MESSAGE\", \"UNAUTHORIZED\", \"SERVER_ERROR\"]),\n  message: z.string(),\n});\n\n// ============================================================================\n// RPC Examples\n// ============================================================================\n\n/**\n * Get user by ID - example RPC with request/response pattern.\n * Request: GET_USER with { id }\n * Response: USER with { id, name, email }\n */\nexport const GetUser = rpc(\"GET_USER\", { id: z.string() }, \"USER\", {\n  id: z.string(),\n  name: z.string(),\n  email: z.string().optional(),\n});\n\n// ============================================================================\n// Type Exports\n// ============================================================================\n\nexport type { InferMessage, InferPayload, InferResponse } from \"@ws-kit/zod\";\n"
  },
  {
    "path": "packages/ws-protocol/package.json",
    "content": "{\n  \"name\": \"@repo/ws-protocol\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./index.ts\",\n    \"./messages\": \"./messages.ts\",\n    \"./router\": \"./router.ts\",\n    \"./package.json\": \"./package.json\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"example\": \"bun run example.ts\"\n  },\n  \"peerDependencies\": {\n    \"@ws-kit/zod\": \"^0.10.2\",\n    \"zod\": \"^4.3.6\"\n  },\n  \"devDependencies\": {\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/bun\": \"^1.3.9\",\n    \"@ws-kit/bun\": \"^0.10.1\",\n    \"@ws-kit/memory\": \"^0.10.1\",\n    \"@ws-kit/pubsub\": \"^0.10.2\",\n    \"@ws-kit/zod\": \"^0.10.2\",\n    \"typescript\": \"~5.9.3\",\n    \"zod\": \"^4.3.6\"\n  }\n}\n"
  },
  {
    "path": "packages/ws-protocol/router.ts",
    "content": "/**\n * WebSocket router factory for the application.\n *\n * Uses @ws-kit/zod for type-safe message routing with Zod validation.\n * Supports middleware, lifecycle hooks, and pub/sub patterns.\n *\n * @example\n * ```ts\n * import { createBunHandler } from \"@ws-kit/bun\";\n * import { createAppRouter } from \"@repo/ws-protocol/router\";\n *\n * const router = createAppRouter();\n * const { fetch, websocket } = createBunHandler(router);\n *\n * Bun.serve({\n *   port: 3000,\n *   fetch(req, server) {\n *     if (new URL(req.url).pathname === \"/ws\") {\n *       return fetch(req, server);\n *     }\n *     return new Response(\"WebSocket server\");\n *   },\n *   websocket,\n * });\n * ```\n */\n\nimport { createRouter, withZod, type Router } from \"@ws-kit/zod\";\nimport { Ping, Pong, Echo, GetUser } from \"./messages\";\n\n/**\n * Connection data stored per WebSocket connection.\n */\nexport interface AppData extends Record<string, unknown> {\n  connectedAt?: number;\n  userId?: string;\n}\n\n/**\n * Creates the application WebSocket router with message handlers.\n *\n * The router is configured with:\n * - Zod validation plugin for type-safe payload validation\n * - Ping/Pong handlers for connection health checks\n * - Echo handler for testing\n * - GetUser RPC handler as an example\n *\n * @example\n * ```ts\n * const router = createAppRouter();\n *\n * // Add custom handlers\n * router.on(CustomMessage, (ctx) => {\n *   // Handle custom message\n * });\n * ```\n */\nexport function createAppRouter(): Router<AppData> {\n  const router = createRouter<AppData>()\n    .plugin(withZod())\n\n    // =========================================================================\n    // Lifecycle Hooks\n    // =========================================================================\n\n    .onOpen((ctx) => {\n      ctx.assignData({ connectedAt: Date.now() });\n      console.log(`[WS] Client connected: ${ctx.clientId}`);\n    })\n\n    .onClose((ctx) => {\n      const connectedAt = ctx.data.connectedAt ?? Date.now();\n      const duration = Math.round((Date.now() - connectedAt) / 1000);\n      console.log(\n        `[WS] Client disconnected: ${ctx.clientId} after ${duration}s`,\n      );\n    })\n\n    .onError((error) => {\n      console.error(\"[WS] Error:\", error);\n    })\n\n    // =========================================================================\n    // Message Handlers\n    // =========================================================================\n\n    .on(Ping, (ctx) => {\n      ctx.send(Pong, { timestamp: Date.now() });\n    })\n\n    .on(Echo, (ctx) => {\n      ctx.send(Echo, { text: ctx.payload.text });\n    })\n\n    // =========================================================================\n    // RPC Handlers\n    // =========================================================================\n\n    .rpc(GetUser, async (ctx) => {\n      // Example: fetch user from database\n      // const user = await db.query.users.findFirst({\n      //   where: eq(users.id, ctx.payload.id),\n      // });\n\n      // Mock response for demonstration\n      ctx.reply({\n        id: ctx.payload.id,\n        name: `User ${ctx.payload.id}`,\n        email: `user-${ctx.payload.id}@example.com`,\n      });\n    });\n\n  return router;\n}\n\n// Re-export for convenience\nexport { createRouter, withZod } from \"@ws-kit/zod\";\n"
  },
  {
    "path": "packages/ws-protocol/tsconfig.json",
    "content": "{\n  \"extends\": \"../typescript-config/node.jsonc\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./\"\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"**/node_modules/**/*\", \"dist\"]\n}\n"
  },
  {
    "path": "scripts/mcp.ts",
    "content": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { fileURLToPath } from \"bun\";\nimport { execa } from \"execa\";\nimport { z } from \"zod/v3\";\n\nconst rootDir = fileURLToPath(new URL(\"..\", import.meta.url));\nconst $ = execa({ cwd: rootDir });\n\n/**\n * Model Context Protocol (MCP) server.\n *\n * @see https://modelcontextprotocol.org\n * @see https://code.visualstudio.com/docs/copilot/chat/mcp-servers\n */\nconst server = new McpServer({\n  name: \"React Starter Kit\",\n  version: \"0.0.0\",\n});\n\n// This is just an example of a custom command that can be executed\n// from the MCP client (e.g., VS Code, GitHub Copilot, etc.).\nserver.tool(\n  \"eslint\",\n  \"Lint JavaScript and TypeScript files with ESLint\",\n  { filename: z.string() },\n  async ({ filename }) => {\n    const cmd = await $`bun run eslint ${filename}`;\n    return { content: [{ type: \"text\", text: cmd.stdout }] };\n  },\n);\n\n// Start receiving messages on stdin and sending messages on stdout\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n"
  },
  {
    "path": "scripts/package.json",
    "content": "{\n  \"name\": \"@repo/scripts\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.26.0\",\n    \"execa\": \"^9.6.1\",\n    \"got\": \"^14.6.6\",\n    \"zod\": \"^4.3.6\",\n    \"zx\": \"^8.8.5\"\n  },\n  \"devDependencies\": {\n    \"@repo/typescript-config\": \"workspace:*\",\n    \"@types/bun\": \"^1.3.9\",\n    \"typescript\": \"~5.9.3\"\n  }\n}\n"
  },
  {
    "path": "scripts/post-install.ts",
    "content": "import { execa } from \"execa\";\nimport { existsSync } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { EOL } from \"node:os\";\n\n// Create Git-ignored files for environment variable overrides\nif (!existsSync(\"./.env.local\")) {\n  await writeFile(\n    \"./.env.local\",\n    [\n      `# Overrides for the \\`.env\\` file in the root folder.`,\n      \"#\",\n      \"# CLOUDFLARE_API_TOKEN=xxxxx\",\n      \"#\",\n      \"\",\n      \"API_URL=http://localhost:8080\",\n      \"\",\n    ].join(EOL),\n    \"utf-8\",\n  );\n}\n\ntry {\n  await execa(\"bun\", [\"run\", \"tsc\", \"--build\"], { stdin: \"inherit\" });\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n} catch (err) {\n  // console.error(err);\n}\n"
  },
  {
    "path": "scripts/tsconfig.json",
    "content": "{\n  \"extends\": \"../packages/typescript-config/node.jsonc\",\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"noEmit\": true,\n    \"tsBuildInfoFile\": \"../.cache/tsconfig/scripts.tsbuildinfo\"\n  },\n  \"include\": [\"**/*.ts\", \"**/*.js\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./apps/app\" },\n    { \"path\": \"./apps/api\" },\n    { \"path\": \"./apps/email\" },\n    { \"path\": \"./apps/web\" },\n    { \"path\": \"./db\" },\n    { \"path\": \"./packages/core\" },\n    { \"path\": \"./packages/ui\" },\n    { \"path\": \"./packages/ws-protocol\" },\n    { \"path\": \"./scripts\" }\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\n/**\n * Vitest configuration.\n *\n * @see https://vitest.dev/config/\n */\nexport default defineConfig({\n  cacheDir: \"./.cache/vite\",\n  test: {\n    projects: [\"apps/api\", \"apps/app\"],\n  },\n});\n"
  }
]