Cella is a TypeScript template to build web apps with sync engine for offline and realtime use. Postgres, openapi & react-query are foundational layers.
Cella is an implementation-ready template with quite some modules and a default entity config, see shared/default-config.ts and shared/hierarchy-config.ts. Each fork will extend and change the entity config and its hierarchy, so its important to write entity-agnostic code.
See info/ARCHITECTURE.md for tech stack, file structure, data modeling, security, and sync/offline design.
- Backend (Hono + OpenAPI):
backend/src/server.tscreates the base app, mounts global middleware and the error handler (appErrorHandler).- Routes:
backend/src/modules/<module>/<module>-routes.tsusingcreateXRoute. - Handlers:
backend/src/modules/<module>/<module>-handlers.tsusing.openapi()onOpenAPIHono. - All module handlers are chained in
backend/src/routes.tsvia.route(path, handler).
- Frontend (TanStack Router) — code-based, NOT file-based:
- Routes in
frontend/src/routes/*.tsxmust be manually added tofrontend/src/routes/route-tree.tsx. - Layouts in
frontend/src/routes/base-routes.tsx; router instance infrontend/src/routes/router.ts.
- Routes in
Global middleware chain (backend/src/middlewares/app.ts): secureHeaders → OpenTelemetry → observability → Sentry → pino logger → CORS → CSRF → body-limit → gzip.
Route-level guards in backend/src/middlewares/guard/ control auth and tenant isolation:
authGuard: Validates session, setsctx.var.user,ctx.var.memberships,ctx.var.db(baseDb).tenantGuard: Verifies tenant membership, wraps handler in an RLS-enabled transaction — setsctx.var.dbto a transaction withSET LOCALRLS session variables (tenant_id,user_id,role). This is how RLS policies are enforced: all handler DB access goes throughctx.var.db.orgGuard: Resolves organization and verifies membership within the tenant transaction.publicGuard: For unauthenticated routes, setsctx.var.dbto baseDb (no RLS transaction).- Also:
sysAdminGuard,crossTenantGuard,relatableGuard.
AppError is the structured error class: status, type (i18n key from locales/en/error), severity, entityType, meta. PostgreSQL error codes are mapped automatically (FK violation → 400, unique constraint → 409, RLS denial → 403, deadlock → 409). The global handler appErrorHandler is registered in backend/src/server.ts.
Auth is split into five sub-modules in backend/src/modules/auth/: general/ (session, cookies, MFA, verification emails), passwords/, oauth/, passkeys/ (WebAuthn), totps/ (TOTP 2FA). Session management lives in general/helpers/session.ts; cookie handling in general/helpers/cookie.ts.
The permission system (in backend/src/permissions/) provides: checkPermission (membership + role checks with hierarchy traversal), canAccessEntity, canCreateEntity, getValidContextEntity, getValidProductEntity (fetch + permission check), splitByPermission (batch filtering). Access policies are defined using configureAccessPolicies() in shared/permissions-config.ts with three values: 1 (allowed), 0 (denied), 'own' (allowed only for the entity's creator — implicit owner relation). The engine checks entity.createdBy === userId for 'own' policies. On the frontend, computeCan() produces a three-state map (true | false | 'own'); use resolvePermission() from shared to resolve 'own' per-entity. Guards invoke these functions; see ARCHITECTURE.md for defense-in-depth layers (Permission Manager → RLS → composite FKs).
- Server state: TanStack Query (
offlineFirstnetwork mode, IndexedDB persistence viaPersistQueryClientProvider). Query options/keys/mutations infrontend/src/modules/<module>/query.ts. Paused mutations resume after reload via mutation registry (frontend/src/query/mutation-registry.ts). See ARCHITECTURE.md "Query layer" section for full architecture. - Client state: Zustand stores in
frontend/src/store/. - API client: Generated SDK in
frontend/src/api.gen/. Never modify manually — runpnpm generate:openapiafter backend route/schema changes. - DB schemas:
backend/src/db/schema/(Drizzle ORM). Runpnpm generatefor migrations. - API validation: Zod schemas in
backend/src/modules/<module>/<module>-schema.ts(using@hono/zod-openapi). Shared base schemas inbackend/src/schemas/. - Frontend types: Generated in
frontend/src/api.gen/; module-specific types infrontend/src/modules/<module>/types.ts. - Types are inferred from Zod schemas (
z.infer). Avoidastype assertions — preferObject.assign,satisfies, oras const.
- Query keys: Use
createEntityKeys<Filters>('myEntity')and register withregisterEntityQueryKeys('myEntity', keys)in the module'squery.ts. Keys follow[entityType, 'list'|'detail', ...]convention. - Optimistic updates: Use
useMutateQueryData(queryKey)for cache mutations. Generate placeholder entities withcreateOptimisticEntity(zodSchema, overrides)— it auto-fills IDs, timestamps, and Zod defaults. - Invalidation: Use
invalidateIfLastMutation(queryClient, mutationKey, queryKey)inonSettledto prevent over-invalidation when multiple mutations are in flight. - Mutation registry: In each entity's
query.ts, calladdMutationRegistrar((qc) => { qc.setMutationDefaults(keys.create, { mutationFn: ... }) })so paused offline mutations can resume after reload. - Enrichment: Context entity list items are auto-enriched with
item.membership,item.can(permission map), anditem.ancestorSlugsvia a QueryCache subscriber infrontend/src/query/enrichment/. No manual wiring needed — just ensure query keys are registered. - Slug resolution: Use
fetchSlugCacheId(fetcher, cacheKey)to resolve slug-based routes to IDs and cache the result under the entity's detail key.
Extension system (backend/src/docs/):
x-middleware.ts: Wrap guards/limiters/caches withxMiddleware(options, fn)— they auto-appear in the spec and docs UI. UsesetMiddlewareExtensionfor composed middleware.x-routes.ts: Always usecreateXRouteinstead ofcreateRoute. Props:xGuard(required),xRateLimiter,xCache.extensions-config.ts: Add newx-*extension types here.docs.ts: Orchestrates spec build, writesopenapi.cache.json, mounts Scalar at/docs.- Frontend: Vite plugin in
frontend/vite/openapi-parser/→ output infrontend/public/static/docs.gen/. Docs UI infrontend/src/modules/docs/.
Mocks (backend/mocks/mock-*.ts, utils in backend/mocks/utils/):
- Each entity has insert mocks (
mockUser()→Insert*Model) and response mocks (mockUserResponse()→ deterministic viawithFakerSeed). - OpenAPI examples: pass
mockXResponse()to.openapi('Name', { example })and routeexample:. - Seeding (
backend/scripts/seeds/): callsetMockContext('script')+mockMany(mockEntity, count). - Tests (
backend/tests/): use insert mocks viabackend/tests/helpers.ts. CallresetXMockEnforcers()in cleanup. - Key utils:
mockMany(),mockPaginated(),mockTimestamps(),pastIsoDate(),mockContextEntityIdColumns().
- Stx helpers (
frontend/src/query/offline/):createStxForCreate(),createStxForUpdate(),createStxForDelete()build sync transaction metadata from cached entity version. - Conflict detection:
checkFieldConflicts()compares per-field versions;isTransactionProcessed()checks idempotency viaactivitiestable. - Realtime backend (
backend/src/sync/):activityBus→createStreamDispatcher()→streamSubscriberManagerfor SSE fan-out.CdcWebSocketServeraccepts the CDC worker connection on/internal/cdc. - Realtime frontend (
frontend/src/query/realtime/): Two streams —AppStream(authenticated, leader-tab via Web Locks + BroadcastChannel, echo prevention viastx.sourceId, catchup viaseqdelta) andPublicStream(unauthenticated, per-tab connection, catches up deletes on connect then live-only). - Seen-by tracking: Frontend marks entities seen via
IntersectionObserver, batches IDs in a Zustand store, flushes on timer +sendBeaconon unload. Flushed IDs persist in localStorage. Unseen badges are optimistically decremented in React Query cache. Backend:seen_bytable (one row per user+entity),seen_counts(denormalized view count). - Entity cache: CDC-invalidated in-memory cache in
backend/src/middlewares/entity-cache/.coalesce()deduplicates concurrent fetches.
- Entities:
ContextEntityType(has memberships) andProductEntityType(content-related). Seeinfo/ARCHITECTURE.md. - Configuration:
shared/default-config.tsdefines the base config (validated againstRequiredConfig). Per-deploy overrides (e.g.shared/development-config.ts) deep-merge over it, selected byNODE_ENVinshared/app-config.ts. Check.envfor secrets and environment variables. - Debug mode: Set
VITE_DEBUG_MODE=trueinfrontend/.env. - Stores, no Providers: Favour Zustand stores over React Provider pattern.
- OpenAPI nullable: Use
z.union([schema, z.null()])instead ofschema.nullable()for named schemas. - OpenAPI schema naming: Only register schemas as named components (
.openapi('Name')) for core entity responses or shared base types. Inline enums and request body schemas. Share a single schema when shape is identical across contexts.
- Formatter/Linter: Biome (
biome.json). Runpnpm lint:fix. - Indentation: 2 spaces; line width: 100; quotes: single; semicolons: as needed; trailing commas: ES5.
- Zod v4 only:
import { z } from 'zod'. In backend:import { z } from '@hono/zod-openapi'. - camelCase for variables/functions (including constants), PascalCase for components, kebab-case for files, snake_case for translation keys.
- JSDoc on all exports. Backend: full JSDoc with params/response. Frontend: 1-3 lines. No standalone file-level comments above imports.
- Storybook: Stories in
stories/folder within the module, named<component-filename>.stories.tsx. - Icons: lucide with Icon suffix (e.g.,
PencilIcon). - Keep existing code comments intact unless cleanup is explicitly requested.
- Console:
console.logfor temp debugging (remove before commit),console.infofor logging,console.debugfor dev (stripped in prod). - Links as buttons: Use
<Link>withbuttonVariants()for linkable actions. Allow new-tab opening for URL-targetable sheet content. - React-compiler:
useMemo/useCallbackcan be avoided in most cases. - Translations: All UI text via
useTranslation()andt('namespace:key'). Never hardcode. Files inlocales/en/.
- Framework: Vitest. Name tests
*.test.ts; place near source or undertests/. - See info/TESTING.md for test modes and detailed documentation.
- Use
gitandghCLI. Conventional Commits:feat:,fix:,chore:,refactor:. - PRs: concise description, linked issues, passing checks. Keep changes scoped.
pnpm quick: Dev with PGlite (fast, no Docker).pnpm dev:core: Dev with PostgreSQL (no CDC).pnpm dev: Dev with PostgreSQL + CDC Worker.pnpm check: Runsgenerate:openapi+ typecheck +lint:fix.pnpm generate: Create Drizzle migrations from schema changes.pnpm generate:openapi: Regenerate OpenAPI spec and frontend SDK.pnpm seed: Seed database with test data.pnpm test: Run all tests (alias fortest:core). Also:test:basic(no Docker),test:full(includes CDC).pnpm cella: run Cella CLI to sync with upstream or downstream: cli/cella/README.md