@apollo-deploy/schemas
v1.7.0
Published
Cross-language type definitions using JSON Schema for Apollo Deploy monorepo
Readme
@apollo-deploy/schemas
Shared wire contracts for Apollo Deploy. This package is the single source of truth for types that cross the backend↔client boundary.
If a schema is only used inside one backend service, it does not belong here.
TL;DR (Read This First)
- This package is for shared wire contracts only — request/response shapes, public domain models, and enums consumed by both the backend and at least one external surface (SDK, web client, OpenAPI export, another service, or non-TS service).
- Backend-private schemas live in the owning module, not here. For
apps/api, that isapps/api/src/modules/<module>/contracts/internal/. - Only
contracts/global/*inside an api module may import from this package. Other layers must go through the module'scontracts/globalwrapper. - After changing schema sources, run, in order:
registry:check→generate→build. - When unsure whether something is "shared", default to keeping it local. Promoting later is cheap. Un-promoting after consumers depend on it is not.
What This Package Is For
@apollo-deploy/schemas defines:
- Zod schemas for runtime validation and TypeScript type inference
- JSON Schema files for non-TypeScript consumers (Zig, Rust, Go, etc.)
- Stable, versioned wire contracts that the backend and its clients agree on
It is consumed by:
apps/api— for request/response validation and OpenAPI generationpackages/typescript-sdk— generated from these contracts- The web client and other first-party clients
- Non-TypeScript services that read the generated JSON Schema files
It is not a general-purpose type library. It is a contract surface.
When To Add A Schema Here
Add a schema to packages/schemas only if all of the following are true:
- It defines data that crosses a process boundary (HTTP request/response, queue payload, webhook body, etc.).
- It is consumed (or will be consumed) by at least one non-backend surface — SDK, web client, OpenAPI, third-party service, or non-TS service.
- It needs both runtime validation (Zod) and a published TypeScript type.
- It is intended to be stable — breaking changes require coordination with consumers.
Common examples that belong here:
- Public REST request bodies, query params, path params, and response envelopes
- Public domain entities exposed in API responses (
AppSchema,ReleaseSchema, etc.) - Enums shared across request and response (
ReleaseStatusSchema,PlatformSchema) - Webhook payload shapes
- Anything that needs JSON Schema output for cross-language consumers
When Not To Add A Schema Here
Keep the schema local to the owning module if any of the following apply:
- It is only used inside the backend (admin endpoints used by internal tooling are still backend-private unless that tooling is a separate published client).
- It is a persistence-layer or domain-model type that is not part of any wire contract.
- It is a transport-only request shape for a single internal route with no SDK/client consumer.
- It is a temporary adapter, an in-flight refactor, or experimental.
- It is a service-internal queue/job payload not exposed externally.
For apps/api modules the canonical home for these is:
apps/api/src/modules/<module>/contracts/internal/<name>.schema.tsInternal contracts MUST NOT be imported from @apollo-deploy/schemas, MUST NOT be re-exported from this package, and MUST NOT be referenced by other services. If an internal schema later needs to be shared, promote it: move it here, register it, regenerate, and update the module's contracts/global wrapper to consume the shared version.
Decision Tree
Is this schema crossing a process boundary?
├── No → Keep it in the owning module (domain/, application/, or contracts/internal).
└── Yes → Will any non-backend surface consume it?
(SDK, web client, OpenAPI, third-party, non-TS service)
├── No → Keep it in contracts/internal.
└── Yes → Add it to packages/schemas.When the answer is unclear, keep it local. Promotion is a one-line move; rollback is not.
How apps/api Modules Consume This Package
Per the workspace architecture (see AGENTS.md):
- Only
contracts/global/*inside a feature module may import from@apollo-deploy/schemas. - All other layers (controllers, services, infrastructure, route schemas) import from the module's
contracts/globalbarrel. - This keeps the dependency on shared contracts explicit and easy to audit per module.
// apps/api/src/modules/apps/contracts/global/index.ts
export {
AppSchema,
AppCreateSchema,
AppResponseSchema,
type App,
type AppCreateInput,
} from "@apollo-deploy/schemas/apps";// apps/api/src/modules/apps/api/route-schema.ts
import { AppCreateSchema, AppResponseSchema } from "../contracts/global/index.js";If you find yourself importing @apollo-deploy/schemas directly outside contracts/global/*, stop and route it through the module's barrel.
Installation
Public consumers:
bun add @apollo-deploy/schemas zodInside this monorepo, use the workspace reference:
{ "dependencies": { "@apollo-deploy/schemas": "workspace:*" } }zod >= 4.0.0 is a peer dependency.
Quick Start
Prefer subpath imports for tree-shaking and clarity:
import {
AppCreateSchema,
AppResponseSchema,
type AppCreateInput,
type AppResponse,
} from "@apollo-deploy/schemas/apps";
const input: AppCreateInput = AppCreateSchema.parse(rawBody);Root-level imports are supported but should be reserved for shared primitives:
import {
PaginationSchema,
PaginationMetaSchema,
PlatformSchema,
} from "@apollo-deploy/schemas";Non-TypeScript consumers should use the JSON Schema files under packages/schemas/generated/.
Available Subpaths
The exports map in package.json is the source of truth. Currently published subpaths:
| Domain | Subpath |
|---|---|
| Common (shared primitives, enums, pagination) | @apollo-deploy/schemas |
| All definitions (aggregate barrel) | @apollo-deploy/schemas/definitions |
| Generators (tooling) | @apollo-deploy/schemas/generators |
| Admin Users | @apollo-deploy/schemas/admin-users |
| Analytics | @apollo-deploy/schemas/analytics |
| API Keys | @apollo-deploy/schemas/api-keys |
| Apps | @apollo-deploy/schemas/apps |
| Approvals | @apollo-deploy/schemas/approvals |
| Artifacts | @apollo-deploy/schemas/artifacts |
| Audit Log | @apollo-deploy/schemas/audit-log |
| Auth | @apollo-deploy/schemas/auth |
| Billing | @apollo-deploy/schemas/billing |
| Credentials | @apollo-deploy/schemas/credentials |
| CVE Scanner | @apollo-deploy/schemas/cve-scanner |
| Apollo Signal — Email | @apollo-deploy/schemas/signal-email |
| Integrations | @apollo-deploy/schemas/integrations |
| Marketplace | @apollo-deploy/schemas/marketplace |
| Organizations | @apollo-deploy/schemas/orgs |
| Releases | @apollo-deploy/schemas/releases |
| Service Accounts | @apollo-deploy/schemas/service-accounts |
| Sessions | @apollo-deploy/schemas/sessions |
| Settings | @apollo-deploy/schemas/settings |
| Share Links | @apollo-deploy/schemas/share-links |
| Stores | @apollo-deploy/schemas/stores |
| Teams | @apollo-deploy/schemas/teams |
| Webhooks | @apollo-deploy/schemas/webhooks |
Some directories under src/definitions/ may exist without a published subpath while a domain is still being shaped. Until a subpath is added to package.json, those are not considered shared contracts and external consumers must not depend on them.
Domain Folder Layout
Every domain under src/definitions/<domain>/ follows the same shape:
<domain>/
├── domain.schema.ts # Entities, value objects, shared enums
├── request.schema.ts # Inputs: query, params, bodies
├── response.schema.ts # Outputs: response shapes, list envelopes
├── resources.schema.ts # Optional: large auxiliary schemas tightly coupled to the domain
└── index.ts # Barrel — re-exports only, no definitionsdomain.schema.ts
Stable building blocks. Entities, value objects, and enums shared between request and response. No HTTP concerns.
request.schema.ts
Everything validating data into an endpoint: query params, path params, request bodies. Boundary-only refinements (e.g. coercion, .strict()) live here. Do not redeclare entity fields — compose from domain.schema.ts.
response.schema.ts
Everything describing data out of an endpoint: response bodies, paginated envelopes (typically via createPaginatedResponseSchema), domain-specific pagination metadata when it differs from the common shape.
resources.schema.ts (optional)
Use this when a domain has a secondary concept large enough to clutter domain.schema.ts but still tightly coupled to the domain. Examples in the current codebase:
api-keys/resources.schema.ts— the dynamic permission catalogauth/resources.schema.ts—ROUTE_OAUTH_SCOPES,OAUTH_SCOPE_DESCRIPTIONS,scopeSatisfies
Do not create this file just to split things up. Small domains keep everything in domain.schema.ts.
index.ts
Re-exports only. No definitions, no logic. Apply aliases to avoid cross-domain name collisions (e.g. ApiKeyResponseSchema as OrganizationApiKeyResponseSchema).
Consumers import from the domain barrel or the package root. They must not reach into individual *.schema.ts files.
Authoring Rules
One concern per schema. Keep schemas focused.
Compose, don't duplicate. If a field appears in both request and response, define it in
domain.schema.tsand import it.Reuse common primitives (
UUIDSchema,DateTimeSchema,PaginationSchema, etc.) fromcommon.ts.Document with JSDoc. Every exported schema gets a comment explaining what it represents and when it is used.
Derive nested helper types from exported response contracts rather than redefining client DTOs:
import type { IntegrationsAppIntegration } from "@apollo-deploy/schemas/integrations"; type AppConfigSurface = IntegrationsAppIntegration["appConfig"];Register every exported
*Schemawithz.globalRegistry. The repo enforces this — see the registry section.No HTTP concerns (status codes, framework-specific helpers) inside schema files. Those belong in the consuming module.
Treat exports as a public API. Breaking changes need a deliberate plan. Prefer additive evolution.
Schema Registry
Every exported *Schema constant must be registered with z.globalRegistry. This is automated:
bun run registry:check # Fails if any exported *Schema is unregistered
bun run registry:fix # Auto-adds missing registrationsCI enforces registry:check. Run registry:fix after adding new schemas.
Development Workflow
After changing schema sources, run these in order from packages/schemas/:
bun run registry:check # 1. Confirm all schemas are registered (or run :fix)
bun run generate # 2. Regenerate JSON Schema output under generated/
bun run build # 3. Rebuild dist/ so workspace consumers see new typesThe build step matters in this monorepo: workspace references read declarations from dist/, and stale builds will surface as confusing type errors in apps/api or the SDK.
When removing a schema or subpath, watch for stale files in generated/ and dist/. Source removal does not always delete obsolete artifacts — clean them up explicitly.
Validation Commands
bun run typecheck # Type-only check
bun run test # Bun test suiteAdding A New Domain
Before adding a new domain, confirm it is genuinely shared using the decision tree above. If it is backend-private, create it under the owning module's contracts/internal/ instead.
If it is shared:
Create the domain folder:
mkdir -p packages/schemas/src/definitions/my-domainCreate the standard files:
domain.schema.ts,request.schema.ts,response.schema.ts, optionalresources.schema.ts, andindex.ts.Wire exports:
- Add the domain to
src/definitions/index.ts. - Add a subpath to
package.jsonexports(otherwise external consumers cannot import it). - Add a row to the subpath table in this README.
- Add the domain to
Register schemas:
bun run registry:fix.Generate:
bun run generate.Build:
bun run build.In each consuming
apps/apimodule, expose the new types throughcontracts/global/— never import@apollo-deploy/schemasfrom other layers.
JSON Schema For Non-TypeScript Services
Generated JSON Schema files live in packages/schemas/generated/ and are checked in. Copy or reference them from non-TS services:
// Zig, Rust, Go, etc. consume the *.schema.json files directly.
// See generated/ for the latest output.Do not hand-edit files in generated/. They are produced by bun run generate.
Anti-Patterns
// ❌ Adding a backend-only admin endpoint schema here
// Belongs in apps/api/src/modules/<module>/contracts/internal/
// ❌ Importing from @apollo-deploy/schemas anywhere except contracts/global/*
// in an apps/api module
import { AppSchema } from "@apollo-deploy/schemas/apps"; // in controller.ts
// ✅ Route through the module's contracts/global barrel
import { AppSchema } from "../contracts/global/index.js";// ❌ Redefining a published response shape in a client
type AppListResponse = { items: App[]; nextCursor?: string };
// ✅ Derive from the published contract
import type { AppListEnvelope } from "@apollo-deploy/schemas/apps";
type AppListResponse = AppListEnvelope;// ❌ Inline definitions or logic in index.ts
export const Foo = z.object({ ... });
// ✅ index.ts re-exports only
export * from "./domain.schema.js";
export * from "./request.schema.js";
export * from "./response.schema.js";Troubleshooting
Cannot find module '@apollo-deploy/schemas/<domain>'
Add a subpath entry to package.json exports and run bun run build.
Stale types in apps/api after editing a schema
Rebuild the package: bun run build from packages/schemas/. Workspace consumers read dist/.
registry:check fails
Run bun run registry:fix, review the diff, commit.
Type errors after removing a schema
Check generated/ and dist/ for stale files. Clean them up and rebuild.
Summary For AI Agents
When deciding where a new schema goes:
- If it is consumed only by
apps/api(including admin/internal routes used by internal tooling that is not a separate published client), put it inapps/api/src/modules/<module>/contracts/internal/. - If it is part of a wire contract consumed by the SDK, web client, OpenAPI export, or any non-backend surface, add it here.
- Inside
apps/apimodules, onlycontracts/global/*may import from@apollo-deploy/schemas. Always route through that wrapper. - After changes:
registry:check→generate→build. - When in doubt, keep the schema local. Promotion is easy; un-promotion is not.
