Skip to content

Repository files navigation

Runsheet

Event guestlist SaaS in a pnpm monorepo.

  • API: @runsheet/api — Fastify, GraphQL Yoga, Drizzle. Producer-only; no background polling loops.
  • Worker: @runsheet/worker — consumes @runsheet/pubsub Valkey-stream deliveries for email and billing outbox rows, and runs scheduled over-limit reconciliation. The domain-event relay code has been removed; the physical domain_events table remains until an owner-authored DROP migration. Running only the API is no longer sufficient for email or billing background work.
  • Web: @runsheet/web — TanStack Start static marketing site for the public acquisition surface.
  • Dashboard: @runsheet/dashboard — Vite, React, TanStack Router, urql, gql.tada.
  • External: @runsheet/external — Vite, React, TanStack Router, TanStack Query. Non-credentialed public runtime for attendee personal links (briefing /b/:accessKey; /a /f /o reserved). Renders its own template renderer.
  • Mobile: @runsheet/mobile — Expo Router, React Native, local-first encrypted SQLite, and persisted GraphQL sync for event-floor operations.
  • Packages: @runsheet/config, @runsheet/database, @runsheet/pubsub, @runsheet/billing, @runsheet/email, @runsheet/builder, @runsheet/ui, @runsheet/i18n, @runsheet/i18n2, @runsheet/validation, @runsheet/observability, @runsheet/tsconfig. @runsheet/i18n is the English-only dashboard/API-adjacent/email translator; @runsheet/i18n2 is the Paraglide localization package used by apps/web. See docs/adr/0001-i18n-package-direction.md.

Prerequisites

  • Node >=24
  • pnpm 11, via Corepack and the root packageManager

Quick Start

pnpm install
cp .env.example .env
cp apps/dashboard/.env.example apps/dashboard/.env
cp apps/web/.env.example apps/web/.env
pnpm codegen
pnpm dev

pnpm dev starts the API, worker, web, dashboard, external, and mobile Metro through Turbo. Run only the API process and email/billing background work will not progress. Mobile requires an installed development build; Expo Go is not supported.

URLs:

Environment

Both services read a single repo-root .env via @runsheet/config (API config lives in apps/api/src/app/config/, worker config in apps/worker/src/config/). Keys are shared by default; the worker reads WORKER_<KEY> in preference to the shared <KEY> (e.g. WORKER_LOG_LEVEL=debug while the API stays at info). See the repo-root .env.example for the full annotated surface.

Variable Required Notes
NODE_ENV no development, test, or production.
HOST no Defaults to 0.0.0.0.
PORT no Defaults to 3000.
DASHBOARD_ORIGIN yes Comma-separated browser origins allowed to call the API with credentials.
DATABASE_URL production / integration Postgres connection string. Production startup requires it; DB-backed tests use TEST_DATABASE_URL.
TOKEN_SECRET production Secret for auth cookies/JWTs, token HMACs, OAuth state cookies, and unsubscribe tokens. A dev-only fallback is used outside production.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_OAUTH_REDIRECT_URI production Google OAuth credentials and callback URL.
S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY production S3-compatible storage bucket and credentials.
S3_ENDPOINT, S3_REGION, S3_FORCE_PATH_STYLE, S3_PUBLIC_BASE_URL S3_PUBLIC_BASE_URL in production S3-compatible storage endpoint options and the stable public-read base URL for public-safe assets.
MAIL_FROM_ADDRESS, MAIL_FROM_NAME, MAIL_REPLY_TO, HMAC_INTERNAL_SECRET, MAIL_PUBLIC_API_ORIGIN, ENCRYPTION_SECRET production-dependent Transactional email producer values used at request/enqueue time. The API encrypts outbox bodies; the worker decrypts before sending. ENCRYPTION_SECRET is set once in the root .env, so the API and worker can never drift.
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_* production Stripe request-time API and webhook signature verification.

Worker variables are read in apps/worker/src/config/. The worker hosts the @runsheet/pubsub Valkey stream consumer for email/billing outbox trigger messages and the scheduled over-limit reconciliation loop. The old domain-event relay code is removed; see docs/pubsub-migration.md for the retained table cleanup note. See the repo-root .env.example and docs/dev/async-jobs-and-events.md for the full surface and feature flags. Legacy *_INPROCESS=false env vars remain as mixed-version disable overrides.

Dashboard variables are read only through apps/dashboard/src/lib/env.ts.

Variable Required Notes
VITE_API_URL production / staging builds API base origin, for example http://localhost:3000 in development. Production and staging dashboard builds fail when this is missing or points at localhost.

Web variables are public build-time values under apps/web/.env.example. Route metadata and emitted Pages assets control indexing behavior.

Scripts

Command Purpose
pnpm dev Run all apps, including mobile Metro, through Turbo.
pnpm dev:api Run just the API.
pnpm --filter @runsheet/worker dev Run just the worker.
pnpm dev:web Run the marketing site on port 3400.
pnpm dev:dashboard Run the dashboard on port 3300.
pnpm dev:external Run the external public app on port 3401.
pnpm dev:mobile Start Metro for an installed mobile development build.
pnpm storybook Run dashboard Storybook on port 6006, restarting it if it exits unexpectedly.
pnpm build Build all workspaces.
pnpm typecheck Run gql.tada check, then TypeScript checks.
pnpm codegen Regenerate API GraphQL, dashboard/mobile gql.tada and persisted-operation artifacts, plus app route trees.
pnpm codegen:check Regenerate committed artifacts and fail if they drift.
pnpm test / pnpm test:unit Run unit and non-DB workspace tests.
pnpm test:integration Run DB/app-backed API integration tests.
pnpm test:all Run unit plus integration tests.
pnpm db:generate Generate Drizzle migrations for the API.
pnpm db:check Check Drizzle migration history consistency.
pnpm db:migrate Apply Drizzle migrations for the API.
pnpm db:seed Run the API seed script.
pnpm db:studio Open Drizzle Studio for the API.
pnpm lint / pnpm lint:fix / pnpm lint:fix:unsafe Run Biome lint tasks.
pnpm format:fix Run Biome format and organize imports.
pnpm check / pnpm check:write Run Biome check at the repo root.
pnpm clean / pnpm clean:all Remove ignored build output, or also root node_modules.

Repository Layout

apps/
  api/
    run.ts                  # process entrypoint, initializes observability
    seed.ts                 # local seed CLI
    schema/                 # GraphQL SDL split files
    generated/              # committed GraphQL generated artifacts
    src/
      server.ts             # process lifecycle
      app/                  # config, app context, Fastify composition
      core/                 # shared infrastructure
      modules/              # domain behavior and resolver/route modules
  worker/
    run.ts                  # worker process entrypoint
    src/
      server.ts             # process lifecycle: Valkey stream consumers + over-limit loop
      config/               # worker env validation (per-domain slices via @runsheet/config)
      shutdown.ts           # signal handlers
      email/, billing/, events/, handlers/, pubsub/  # runtime wiring per concern
  web/
    src/
      routes/               # TanStack Start file routes
      routeTree.gen.ts      # committed TanStack Router generated artifact
      components/, config/, lib/
  dashboard/
    graphql-env.d.ts        # committed gql.tada introspection
    src/
      app/                  # providers, router, bootstrap
      routes/               # TanStack Router file routes
      modules/              # dashboard domain modules
      components/           # app-level reusable UI
      graphql/              # handwritten gql.tada documents
      lib/                  # browser/client helpers
  external/
    src/
      app/                  # query-client, providers, router
      routes/               # TanStack Router file routes (b.$accessKey, index)
      pages/                # surface page logic (briefing-page, briefing-states)
      lib/                  # api client, queries, access-key, env, cn
      components/           # external-state-page, powered-by-runsheet, template-renderer/
      routeTree.gen.ts      # committed TanStack Router generated artifact
  mobile/
    graphql-env.d.ts        # committed gql.tada introspection
    src/
      app/                  # Expo Router routes
      db/                   # encrypted SQLite schema + generated migrations
      features/             # local-first feature repositories and UI
      sync/                 # pull, push, outbox, and conflict handling
packages/
  database/                 # Drizzle schema, migrations, repos, DB provider
  pubsub/                   # provider-agnostic messaging (topics, envelope, Valkey provider)
  billing/                  # Stripe client + billing outbox + over-limit
  email/                    # email templates + provider + outbox worker runtime
  builder/                  # Node/Docker bundle helpers
  ui/
  i18n/                     # English-only product-app/API/email translations
  i18n2/                    # Paraglide-powered web localization
  config/
  validation/
  observability/
  tsconfig/
docs/
  product/                  # product intent
  domains/                  # domain model and behavior intent
  dev/                      # implementation docs
  specs/                    # implementation slices
  tasks/                    # implementation task plans

GraphQL Workflow

The API owns schema SDL in apps/api/schema/*.schema.graphql. Codegen merges the SDL and writes:

  • apps/api/generated/resolvers.ts
  • apps/api/generated/type-defs.ts
  • apps/api/generated/schema.graphql

The dashboard uses gql.tada. Operation documents are handwritten under apps/dashboard/src/graphql/{fragments,queries,mutations} and validated against apps/dashboard/graphql-env.d.ts.

Fragments imported from another file must be passed as the second argument to the graphql helper:

export const ViewerQuery = graphql(
  `query Viewer { me { ...UserFields } }`,
  [UserFieldsFragment],
);

Dashboard components import module API hooks. Hooks import GraphQL documents. Components do not import raw GraphQL documents, @/lib/graphql, or gql.tada.

After API schema changes, run:

pnpm codegen
pnpm typecheck

Path Aliases

Alias App Resolves to
@/* API apps/api/src/*
@test/* API tests apps/api/test/*
@generated/resolvers API apps/api/generated/resolvers.ts
@generated/type-defs API apps/api/generated/type-defs.ts
@/* Dashboard apps/dashboard/src/*
@/* Web apps/web/src/*
@/* External apps/external/src/*
@runsheet/<pkg> all workspaces workspace packages under packages/*

Do not deep-import package internals; import through each package's public entrypoint.

Logging

The API initializes observability explicitly in apps/api/run.ts through initObservability(...). Application code imports logger from @runsheet/observability; inside GraphQL request scope it resolves to the request-bound logger.

Never log GraphQL variables, tokens, cookies, password hashes, refresh-token hashes, raw passwords, signed URLs, storage keys, or secrets.

Documentation Status

Code is the source of truth for implemented behavior.

  • docs/dev/operating-manual.md is the canonical engineering manual; root AGENTS.md and CLAUDE.md are entry points for agent tooling.
  • docs/product/* and docs/domains/* describe product/domain intent.
  • Active implementation work should use the current spec/task set under docs/specs and docs/tasks.
  • Completed specs and tasks should be treated as historical baselines, not current implementation truth when they conflict with code.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages