Skip to content

About

TanStack Start app with Query, Neon, Better Auth, Drizzle, Shadcn — deployed to Railway

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1,709 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Reputation Key

A reputation management platform built with TanStack Start, Better Auth, Drizzle ORM, and PostgreSQL.

Proprietary software. Copyright © 2026 Fortunatus EOOD. All rights reserved. Reputation Key is not open source: no license is granted to use, copy, modify or distribute this code. See LICENSE.

Authority: docs/BETA.md is the one page that governs the closed beta — what it is, what we owe Google and users, what the product does, and which rules are enforced by which test. It outranks every other document here.

Quick Start

# 0. Use the pinned Node runtime — 22.23.4, exactly (.nvmrc)
fnm use            # or: nvm use
pnpm local:doctor  # Docker, published stack ports, stale containers, VM headroom

# 1. Install dependencies
pnpm install

# 2. Set up environment
cp .env.example .env.local
# Edit .env.local with your DATABASE_URL and BETTER_AUTH_SECRET

# 3. Set up the database through the production-equivalent authority
# (pinned Better Auth schema, the Drizzle journal, and provider-subject
# initialization). DEPLOY_MIGRATE=1 is the explicit local/CI authority;
# Railway deployments use platform identity instead.
# On an empty database, also set one sealed
# REVIEW_PROVIDER_SUBJECT_HMAC_MIGRATOR_KEYS entry in .env.local.
DEPLOY_MIGRATE=1 pnpm db:migrate-deploy

# 4. Generate auth secret (if not set)
node --input-type=module -e "import { randomBytes } from 'node:crypto'; console.log(randomBytes(32).toString('base64url'))"

# 5. Start dev server
pnpm dev

Database Migrations

Two migration systems run in a fixed order, both behind one command:

DEPLOY_MIGRATE=1 pnpm db:migrate-deploy

It provisions the pinned Better Auth tables, applies the Drizzle journal, performs provider-subject initialization, and then applies the declared capability posture (a no-op unless GOOGLE_CONTENT_CAPABILITIES_ALLOWED / AI_CAPABILITIES_ENABLED are set — see "Capability kill switches after a database reset" below). Railway invokes the same runner under platform identity instead of setting DEPLOY_MIGRATE. Use this same journaled workflow in development, CI and production. Never run pnpm db:push against this schema — it bypasses the authoritative journal and conceals deploy-time drift.

  • Auth schema change: edit src/shared/auth/org-schema.ts (the single source for additionalFields), then pnpm auth:migrate.
  • Business schema change: edit src/shared/db/schema/, write the migration by hand as the next drizzle/NNNN_<name>.sql, append its entry to drizzle/meta/_journal.json, then run DEPLOY_MIGRATE=1 pnpm db:migrate-deploy and pnpm check:schema-drift. The journal is history: the deployed database has applied it and keeps its data, so never regenerate it. pnpm db:baseline refuses while an appended migration exists. Steps and journal rules: src/shared/db/CONTEXT.md, "Change loop".

CI runs pnpm db:migrate-deploy (.github/workflows/ci.yml, Predeploy migration parity). Schema authority and current deploy order: src/shared/db/CONTEXT.md.

Architecture

  • Web app: TanStack Start (React + SSR) — pnpm dev / pnpm build / pnpm start
  • Worker: Plain Node.js script — built by pnpm build, run with pnpm start:worker
  • Database: PostgreSQL via Drizzle ORM
  • Auth: Better Auth with DB-backed sessions
  • Redis: Optional in basic development; physically separate Cache Redis and Queue Redis are required in production

Scripts

Command Description
pnpm dev Start dev server on :3000
pnpm local:up Inner loop: services in Compose, web (HMR) + worker on the host
pnpm local:down Remove the local services and their volumes
pnpm build Build the web, worker, and migration bundles
pnpm start Run built web server
pnpm start:worker Run built worker
pnpm test:unit Run unit tests
pnpm test:e2e Run Playwright E2E tests
pnpm typecheck TypeScript check: src + e2e, tooling (tsconfig.scripts.json), project-coverage guard
pnpm lint ESLint + filename/component-boundary checks
pnpm lint:ci lint + test-quality, Google ids, env contract, migration-journal, length/index ratchets
pnpm format Prettier format

Local stacks

Both stacks share the committed e2e/stack.env and the same Compose services: Postgres, Redis, the TLS Google Business Profile sandbox, the AI provider stub and the mail stub. scripts/e2e/stack-services.sh issues a run-scoped local CA with openssl into the ignored e2e/.certs/ (the sandbox is TLS-only, ADR 0050), starts the services, runs the deploy migration and the e2e seed. Nothing here talks to real Google, OpenAI or Resend.

Inner loop - pnpm local:up. The web dev server (HMR) and the worker run on the host against those services; every feature runs, including Google import/sync through the sandbox, the AI pipeline (the stub synthesizes answers for unscripted requests) and email. http://127.0.0.1:3000; sign in as test@example.com (password E2E_TEST_PASSWORD in e2e/stack.env) or staff@example.com / password123. To sign in as yourself, set LOCAL_ADMIN_EMAIL (and optionally LOCAL_ADMIN_NAME, LOCAL_ADMIN_PASSWORD): every seed makes that address an AccountAdmin of the seeded organization, with the test@example.com password unless you set one. Ctrl-C stops both processes; pnpm local:down removes the services and their volumes. The host processes resolve the Compose service names to loopback through scripts/local/loopback-hosts.mjs because local-provider-fetch.ts deliberately compiles the AI stub address in. Mirroring the browser console into the terminal is off, because it holds one of the browser's six connections to the app per open tab; set REPKEY_DEVTOOLS_CONSOLE_PIPE=1 (for example in local.env) to turn it on while you keep few tabs open.

Your settings in one place - ~/.config/repkey/local.env. Every checkout and worktree reads this file first, then its own gitignored local.env, which overrides it. Keep real provider credentials and LOCAL_ADMIN_EMAIL there once, instead of copying local.env into each new worktree. The web server listens on the port of BETTER_AUTH_URL, so a second stack can run on another origin; add its callback to the OAuth client's redirect URIs if it uses real Google.

Beta features for an Organization you create by hand. The seeded local admin joins the allowlisted e2e Organization, so Google import, Portals, Goals, AI and notification email work out of the box. An Organization you create yourself is not on BETA_ALLOWLIST_ORGS and every one of those refuses with "not enabled for your organization yet". Add its ID to the list in local.env. Avoid * on a stack you also run e2e against: it lights up the deliberately dark "locked" Organization those tests rely on.

Capability kill switches after a database reset. Below the allowlist sit two database-held kill switches: a fresh database seeds every Google content capability denied (Connect Google then fails with capability_killed) and every AI capability killed. The local seed lifts them for you. A deployed environment declares them instead, on web: GOOGLE_CONTENT_CAPABILITIES_ALLOWED=* and AI_CAPABILITIES_ENABLED=* (or comma lists). Web's deploy step applies the declaration right after migrations, so a reset plus a deploy comes back working. It lifts only an untouched seed default: a denial or kill an operator made stays, and the deploy log says so in one [declared-posture] line. Unset changes nothing; an unknown name fails the deploy (ADR 0032, amended 2026-09-29).

Real Google - local.env + REPKEY_LOCAL_GOOGLE=real. To connect a real Google account and import real locations from this stack, copy local.env.example to local.env (gitignored) and set the real OAuth credentials. pnpm local:up then drops every provider-sandbox endpoint pin, selects Google's approved endpoints (production-fixed), and prints Google: LIVE. Nothing about that path is production-only: outside NODE_ENV=production the four provider keyrings derive local fallbacks from OAUTH_STATE_SECRET and the opaque OAuth state lives in an in-memory provider-ephemeral store.

One Google-side prerequisite, once: an OAuth 2.0 Web application client in the same Google Cloud project as production (the Business Profile API allowlist is per project) whose authorised redirect URIs include exactly

http://127.0.0.1:3000/api/auth/google/callback

The stack sends response_type=code, scope=openid https://www.googleapis.com/auth/business.manage, access_type=offline, prompt=consent, PKCE S256 and an opaque state handle - the same request the deployed cell sends. Without the redirect URI registered, Google answers Error 400: redirect_uri; a wrong client answers invalid_client.

Real API quota is consumed and real reviewer data lands in the local database; pnpm local:down deletes that volume. Re-run the import workflow only on a fresh stack: a portal you created binds the property, and the fixture cleanup cannot delete a bound property. Pub/Sub notifications cannot reach localhost, so review updates arrive through the periodic sync rather than a push. Real AI - REPKEY_LOCAL_AI=real. Independent of the Google posture: the runner drops AI_PROVIDER_LOCAL_STUB so the gateway builds its pinned api.openai.com connector, and refuses to start unless OPENAI_API_KEY looks real, naming the model snapshot the account must be able to call. Analysis, reply drafting and trends then run on the live model; the banner prints AI: LIVE. Spend is bounded by the organisation cost window (ai_organization_cost_windows) and recorded per operation. Verified locally: a reply draft and a worker analysis both settled succeeded for 446 micros total, and the live model classified a review mixed / wait_time / medium where the stub's rating-keyed answer is neutral / service.

In sandbox mode, "Connect Google" cannot work: the authorize host is pinned to accounts.google.com by the provider contract and the sandbox client id is a placeholder, so Google answers Error 401: invalid_client. The stack prints that warning at startup. The seed binds no property to Google, and the sandbox has no default scope. To get a Google-bound property with synced reviews for the AI features, run the import workflow against the running stack:

E2E_EXTERNAL_STACK= NODE_EXTRA_CA_CERTS=e2e/.certs/ca.crt pnpm test:e2e --project=critical e2e/critical/workflows/google-import-sync.spec.ts

Then, in the app: Settings → AI & replies → that property (reply language, enable AI), and a portal's Settings tab for the public display name a reply draft must use.

Pre-merge - pnpm e2e:stack:up. The containerised production build (NODE_ENV=test, E2E=1) on the same services - what CI's e2e job runs. Use it to click through a build or to run the suite; the suite is not meant for the dev server (production server-function ids, the auth rate limit):

pnpm e2e:stack:up
pnpm test:e2e --project=critical
pnpm e2e:stack:down

Use a fresh volume and image lifecycle on every pass when reproducing a flake:

for i in 1 2 3; do pnpm e2e:stack:down && pnpm e2e:stack:up && pnpm test:e2e --project=critical; done

Deploy - pnpm ops deploy-ci-images --sha <main sha> --apply once main is green; CI has already run the same stack, so deploying is not a test step. It ends by asking Google whether it accepts the beta's "Connect Google": the callback ${BETTER_AUTH_URL}/api/auth/google/callback must be an authorised redirect URI on the OAuth client, and the client secret must be current. A refusal (Google's Error 400: redirect_uri_mismatch page, which the app never sees) fails the command with the exact URI to register and the console link. Run the same check alone with pnpm ops check-google-oauth. It also makes sure the object-store bucket's CORS rule lets the app origin PUT (avatar and logo uploads); run that alone with pnpm ops storage-cors [--apply].

A commit whose main CI went green only after Re-run failed jobs (gh run rerun --failed) deploys too. That re-run keeps the images and the digest map an earlier attempt of the same run produced, and the deploy takes that map when the run's latest attempt is green and no later attempt re-ran an image job (docker image group (…) or docker); such a re-run writes a newer map, and the newer map is the one deployed. A map from another run or another commit, or a run whose latest attempt is red, is still refused (scripts/ops/ci-image-digest-map.ts). Re-run the failed jobs, not the whole workflow: a full re-run rebuilds the images, and when a rebuilt digest differs from the one already tagged for the commit, docker fails with immutable image tag has a conflicting digest and the commit cannot deploy. An e2e failure that passed on the re-run gets a row in docs/operations/e2e-flake-register.md.

Git hooks

Husky runs one gate: pre-commit — lint-staged (eslint --fix + prettier --write on staged files). There is no pre-push hook (retired 2026-09-03 as a duplicate of CI); pnpm typecheck and pnpm lint:ci run in CI.

Install hooks after cloning: pnpm install (the prepare script registers Husky automatically).

Project Structure

src/
├── contexts/       # Bounded business domains (ten packages; see root CONTEXT.md)
│   └── <name>/    # Each has: domain/, application/, infrastructure/, server/
├── components/     # React UI
│   ├── ui/        # shadcn primitives
│   ├── forms/     # shared form blocks (SubmitButton, FormErrorBanner, etc.)
│   ├── layout/    # app shell (sidebars, header, top bar)
│   ├── hooks/     # shared hooks (useMutationAction, useAction, usePropertyId)
│   └── features/  # domain-concept folders (portal/, identity/, property/, inbox/, etc.)
├── shared/         # Cross-cutting infrastructure
│   ├── auth/      # Better Auth config, middleware, permissions
│   ├── cache/     # Redis client + cache port/impl
│   ├── config/    # Zod-validated env schema
│   ├── db/        # Drizzle ORM, pool, schema/, migrations
│   ├── domain/    # Brand types, IDs, roles, permissions, clock, Result
│   ├── events/    # Master DomainEvent union and outbox schema registrations
│   ├── jobs/      # BullMQ queue, worker, registry
│   ├── hooks/     # usePermissions
│   ├── observability/ # Pino logger, request tracing (tracedHandler)
│   ├── rate-limit/ # Rate limiting middleware
│   └── testing/   # In-memory port fakes, test fixtures
├── routes/         # TanStack Router file-based routes
│   └── _authenticated/ # Protected routes with layout shell
├── hooks/          # Low-level utility hooks (use-as-ref, use-lazy-ref)
├── lib/            # Shared utilities (utils, compose-refs, lookups)
├── composition.ts  # Dependency wiring
├── bootstrap.ts    # Event/job handler registration
└── worker/         # Background worker entry point

About

TanStack Start app with Query, Neon, Better Auth, Drizzle, Shadcn — deployed to Railway

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages