Surplus logo
Surplus Docsby Sharing Excess
The Stack

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     → types

Three rules of thumb capture it:

  • contracts and types stay free of server-only code, so the client can depend on them safely.
  • services depends on postgres, redis, types, and utils — but never on apps/*.
  • 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.