docs/STRUCTURE.md
Code boundaries
Use the existing directory tree as the file inventory. Add a package only for a real boundary, not as a generic home for helpers.
packages/contractsowns the wire API: one file per domain with its schemas, branded ids and tagged errors, plus the RPC group. Errors carry data, not copy; the UI owns the words. It must not import handlers, database code or Worker bindings.packages/dbowns schema, migrations andDatabase: drizzle's Effect driver over D1, withbatchas the only atomic unit.@tranzfer/db/testinggives tests a fresh migrated local D1.- The top level of
apps/api/srcowns the rules, one domain per file:Deliveries,Transfers,SharedLinks,FileRequests,BillingandPlans(the plan that sets a user's limits, and access codes) services, theStorageport, link tokens and the sweeper.rpc.tsis transport only: read the principal, call a service. apps/api/src/infrastructureowns everything Cloudflare or Alchemy: R2 storage, Better Auth, the stage, Polar access, Axiom telemetry and the browser trace relay. Stage checks live here and nowhere else.apps/web/src/routesowns page and HTTP route entry points.apiowns clients and the Solid bridge.uiowns reused presentation. Keep feature state beside its consumers.infra/alchemy.run.tscomposes the stack.infra/polar-provider.tsholds the Alchemy providers for Polar products and webhook endpoints. The API Worker's resources and bindings live inapps/api/src/resources.tsandindex.ts.
Scope and dependencies
apps/api/src/index.ts is the composition edge. Storage, link tokens and Better Auth are built once per isolate; their secrets and binding values stay lazy, because the same init also runs at deploy time with no env. The D1 client belongs to one invocation, so Database and the domain services over it are built per request and per cron run, with a fresh memo map.
Database failures are defects (dieOnDatabaseError at each service method). Storage failures stay typed as StorageError inside the API and become StorageUnavailable only where a caller can retry.
AuthenticatedLive resolves the incoming cookie and provides CurrentPrincipal. A missing session is Unauthorized; failure to read it is AuthenticationUnavailable. Authentication middleware appends renewed cookies to the HTTP response.
Never capture a request, principal or response headers in an isolate-wide singleton.
Browser RPC posts to exactly /rpc, forwarded by the web Worker to the API binding. SSR creates a request-specific client with incoming cookies and outgoing response headers. See the bridge contract.
Adding an operation
- Define its payload, result and expected errors in the domain's contracts file. Add an error only when the client does something different with it.
- Implement it as a method on the domain service; authorize there, with the sender id the handler passes in.
- Add a one-line handler in
rpc.ts. - Cover it in
apps/api/testagainst the local D1. Transfer changes must also satisfy RELIABILITY.md.