Skip to main content
Ship is a pnpm + Turborepo monorepo, so code shared between apps/api and apps/web lives in one place: the packages/ folder. Define a constant, a UI library, or a config once, depend on it as workspace:*, and every app stays in sync.

What’s in packages/

A scaffolded project ships these workspace packages:
packages/
  app-constants     # shared enums + constants (USER_STATUSES, file rules, token TTLs)
  db                # @ship/db — DbService, the typed Drizzle wrapper
  emails            # @ship/emails — React Email templates (mailer plugin)
  cloud-storage     # @ship/cloud-storage — S3 client (cloud-storage plugin)
  mailer            # Resend + React Email runtime (mailer plugin)
  eslint-config     # shared ESLint config
  prettier-config   # shared Prettier config
  tsconfig          # shared TypeScript base configs
PackageNameProvides
app-constantsapp-constantsCross-app enums and constants — USER_STATUSES, USER_AVATAR, token TTLs. Import these into Zod schemas instead of inlining string literals.
db@ship/dbThe DbService class and MutationEvent types behind the generated @/db service.
emails@ship/emailsReact Email templates, rendered server-side. Added by the mailer plugin.
cloud-storage@ship/cloud-storageThe S3-compatible cloudStorageService. Added by the cloud-storage plugin.
eslint-config / prettier-config / tsconfigsameShared lint, format, and compiler config consumed by every workspace.
There is no shared package. The typed API client is not a package — it lives in the API workspace and reaches the web app through TypeScript declarations. See Typed client, no shared package below.

Using a package

Workspace packages are referenced with the workspace:* protocol. Add one to the dependencies of the app that needs it:
apps/api/package.json
"dependencies": {
  "app-constants": "workspace:*",
  "@ship/db": "workspace:*"
}
Then import it like any module:
import { USER_STATUSES } from 'app-constants';
import { DbService } from '@ship/db';
Run pnpm install once to link the workspace, and the import resolves to your local source — no publish, no build step for the consumer.

Typed client, no shared package

End-to-end type safety does not flow through a shared package. It flows through the API workspace itself:
  1. Endpoints declare their shape with .input(zodSchema).output(zodSchema).
  2. pnpm --filter api build:types emits .d.ts files for the API package.
  3. The web app depends on "api": "workspace:*" and imports the typed oRPC client:
apps/web — typed client
import type { AppClient } from 'api';
Inside the API, codegen keeps the oRPC router, contract, and DbService in lockstep with the filesystem. After adding or removing an endpoint or schema file, run:
pnpm --filter api codegen
That regenerates src/router.ts, src/contract.ts, and src/db.ts from the resources on disk — the types an agent (or you) reads are always real. See How Ship works for the resource model and Schemas for the schema layer.

Adding your own package

Create a folder under packages/, give it a package.json with a workspace:*-friendly name, and add it to whichever app depends on it.
1

Scaffold the package

mkdir -p packages/billing/src
packages/billing/package.json
{
  "name": "@ship/billing",
  "type": "module",
  "version": "0.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts"
}
2

Depend on it

apps/api/package.json
"dependencies": {
  "@ship/billing": "workspace:*"
}
3

Link the workspace

pnpm install
Read more about internal packages in the Turborepo documentation.