Shared packages
The apps stay thin because the reusable code lives in packages/*. Each package has a clear responsibility and a strict rule about what it's allowed to import.
The packages
@surplus/contracts
The single source of truth for the API. It defines every endpoint's method, path, and Zod input/output schema using ORPC. Both the client and the server import it, which is exactly why they can never disagree about an endpoint's shape. It depends only on types, so the client can import it without pulling in any server code. See API and contracts.
@surplus/services
The domain logic, organized as dependency-injected service classes (HubsService, RoutesService, CollectionEventsService, and so on). Methods open a database transaction and call focused service functions. Services receive their dependencies — database, Redis, S3, Stripe, Resend — by injection, and never read environment variables. It also exports the typed error hierarchy (AppError, ValidationError, BusinessRuleError, NotFoundError, ConflictError, ExternalServiceError) and observability helpers.
@surplus/postgres
The Drizzle ORM schema and the Neon client factory. The schema here is the source of truth for the database — tables, columns, enums, relations, and views are all defined in packages/postgres/src/schema/. It exposes a createPostgresClient() factory and the Drizzle query helpers. See Data model.
@surplus/types
Shared Zod schemas and the TypeScript types inferred from them, covering every domain entity. This is the package you derive from when you need a variation of an existing shape — using .pick(), .omit(), .partial(), or .extend() — rather than redefining fields.
@surplus/utils
Pure functions with no I/O and no environment access: formatters, constants (like AUTH_COOKIE_MAX_AGE and MAX_UPLOAD_FILE_SIZE_BYTES), permission definitions, item-quantity math, and business-rule helpers. Because it's pure, it's safe to import anywhere — client, server, or package.
@surplus/redis
The Upstash Redis client factory plus helpers for caching (getCache/setCache), key building, and distributed locks (acquireLock/releaseLock) used by cron jobs.
@surplus/sharedDeps
Curated re-exports of third-party libraries (Drizzle, Zod, jose, Stripe). Centralizing them keeps versions aligned across the workspace and gives a single place to manage upgrades. See Dependencies and supply chain.
How they depend on each other
The dependency graph flows in one direction. Apps depend on packages; packages depend on lower-level packages; nothing depends on apps/*.
apps/client → contracts, types, utils
apps/server → contracts, types, utils, postgres, redis, services
apps/docs → utils
services → postgres, redis, types, utils
postgres → types
contracts → typesThree rules of thumb capture it:
contractsandtypesstay free of server-only code, so the client can depend on them safely.servicesdepends onpostgres,redis,types, andutils— but never onapps/*.- Apps wire concrete clients (database URLs, Redis, S3) and inject them into services.
When you're unsure what a package may import, open its package.json and check the workspace:* dependencies — that's the authoritative list.
Why this matters
This layering is what makes the same domain logic reusable. A service function doesn't know whether it's running inside the API server, a cron worker, or a one-off script — it just receives a transaction and does its work. That's only possible because packages never reach for global configuration or import upward into apps.