This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
make dev-all- Start all dev processes (Docker, type checker, tests) in one commandmake dev- Start local development server with Docker Compose onlymake populate-local-dev- Populate local database with test users (foo@example.com, admin@example.com)./scripts/login.ts admin- Automated login helper for dev server
make check- Run all checks (equivalent to: test, lint, typecheck, unused-exports)make testornpx jest- Run all testsnpx jest path/to/test.test.ts- Run a single test filemake lint- Run ESLintmake typecheck- Type check without emitting filesmake watch-typecheck- Watch mode for type checkingmake unused-exports- Find unused exports
make fix- Auto-fix linting issuesmake smoketest- Run smoke tests- Local app: http://localhost:8080
- Mailcatcher (dev emails): http://localhost:1080
This application uses event sourcing as its core architectural pattern. All state changes are captured as immutable events stored in a SQLite database (via libsql/Turso).
- Located in
src/commands/ - Commands validate authorization and business rules, then produce domain events
- Each command implements the
Command<T>interface with:resource()- identifies which resource the command operates onprocess()- validates business logic and returnsOption<DomainEvent>decode()- validates and decodes input dataisAuthorized()- checks if the actor can execute this command
- Commands NEVER directly query state - they receive all relevant events as input
- See
src/commands/command.tsfor the Command interface
- Defined in
src/types/domain-event.ts - All events extend a base with:
type,actor,recordedAt - Use
constructEvent(eventType)(payload)helper to create new events - Events are immutable and represent facts that occurred
- Event store located in
src/init-dependencies/event-store/ - Optimistic concurrency control via resource versioning
- Located in
src/read-models/ - SharedReadModel (
src/read-models/shared-state/) is the primary read model- Initialized from scratch in-memory on startup using better-sqlite3
- Rebuilt from event store by replaying all events
- Refreshed every 10 seconds via
periodicReadModelRefresh - Also pulls external data (Recurly, Google Sheets) every 60 seconds
- Provides views for members, equipment, areas, super users, etc.
- Uses Drizzle ORM for SQL operations
- Read models project events into queryable views optimized for reads
- Read models are eventually consistent (10s refresh cycle)
- Located in
src/queries/ - Queries read from the SharedReadModel and render HTML responses
- Each query implements the
Querytype (seesrc/queries/query.ts) - Queries receive:
(deps, user, params, queryParams) => TaskEither<FailureWithStatus, HttpResponse>
The codebase maintains strict separation between commands and read models:
- Commands should only use events passed to them, never call read models
- Read models should only process events, never call command.process() (except in tests to generate test events)
- Communication between frontend and backend happens exclusively via events
- This makes testing easier and allows independent evolution of read/write sides
- Magic link authentication (passwordless) via
src/authentication/ - Login requires member number linked to an email address
- Uses Passport.js with custom strategy
- Session management via cookie-session
- Three actor types:
system,token(API),user(logged in member)
- Recurly: Subscription status, payment information
- Google Sheets: Training quiz results and trouble tickets
- Paxton: Access control (read from external service)
- Sync worker (
src/sync-worker/) periodically syncs external data into events
- Event Store: SQLite database (can use Turso for remote persistence)
- Stores all domain events with optimistic concurrency control
- Schema in
src/init-dependencies/event-store/events-table.ts
- Read Model DB: In-memory SQLite rebuilt on startup
- Separate read-only connection for queries
- Schema defined in
src/read-models/shared-state/state.ts
- Google DB: Caches Google Sheets data
- Express.js server in
src/index.ts - Routes defined in
src/routes.ts - HTTP handlers in
src/http/:command-to-handlers.ts- POST handlers that execute commandsquery-to-handler.ts- GET handlers that execute queriesapi-to-handlers.ts- API endpoints with bearer token auth
- Runtime: Node.js 20 with Bun for package management
- Language: TypeScript with strict typing
- Database: SQLite via @libsql/client (Turso compatible)
- ORM: Drizzle ORM for read models
- Functional Programming: fp-ts for TaskEither, Option, pipe patterns
- Validation: io-ts for runtime type checking
- Server: Express.js with Passport for authentication
- Email: MJML for templates, Nodemailer for sending
- Testing: Jest with ts-jest
- Linting: ESLint
- Tests are in
tests/directory (not colocated with source) - Mirror source structure:
tests/commands/,tests/read-models/,tests/queries/ - Use
.test.tsextension
- Provide events as input, assert on resulting events
- Use
command.process({ command, events })pattern - Example:
tests/commands/area/create.test.ts - Use
constructEvent(type)(payload)to build test events - Use
arbitraryActor()fromtests/helpersfor actor values
- Provide events to read model, assert on query results
- It's acceptable to use
command.process()in read model tests ONLY to generate required events - Never call read models from command tests
fakerfor generating test datajest-date-mockfor mocking dates- Use io-ts types (e.g.,
UUID,NonEmptyString) for type-safe test data
TaskEither<Error, Success>for async operations that may failOption<T>for values that may or may not existpipe()for composing operationsTE.tryCatch(),TE.chain(),TE.map()for TaskEither operationsO.some(),O.none,O.isSome()for Option operations
- Helper functions in
src/commands/:is-admin-or-super-user.tsis-equipment-owner.tsis-equipment-trainer.tsis-self-or-privileged.ts
- Authorization checked before command processing via
isAuthorized()
- Members identified by member number (integer)
- Email addresses linked to member numbers via events
MemberLinkingclass in read model handles email ↔ member number lookups
Bearer token authentication required (header: Authorization: Bearer <ADMIN_API_BEARER_TOKEN>):
POST /api/link-number-to-email- Link member number with emailPOST /api/declare-super-user- Grant super user privilegesPOST /api/create-area- Create a new area
See README.md for curl examples.
- Server renders pages server-side with minimal client JS (e.g., GridJS for tables)
- Use
deps.logger(pino) for logging, not console.log - Configuration loaded from environment via
src/configuration.ts - Session secret and token secret must be set in production
- Deployment to Fly.io (makespace-app.fly.dev) and app.makespace.org