@onyx/api
Public API contract defined with oRPC and Zod, plus OpenAPI generation.
@onyx/api is the contract for the public HTTP API: oRPC procedures defined with Zod schemas and REST route metadata, with no server code. The contract is the single source of truth. The Worker already serves it as plain REST under /api/v1, published/client wraps it in the fully typed @jxdltd/onyx-client, and the build regenerates openapi.json for documentation tooling.
Exports
From @onyx/api:
contract: the oRPC contract router. Currently two procedures:health(GET /health, public liveness probe) andorg(GET /org, details of the organization the API key is scoped to).organizationSchemaand theOrganizationtype: the wire shape of an organization. Dates travel as ISO 8601 strings.authErrors: the sharedUNAUTHORIZEDerror declaration, so the server throws exactly the message the contract documents.ApiInputsandApiOutputs: input and output types inferred from the contract.
Authentication
Every non-public endpoint requires an organization API key sent as x-api-key. Session cookies and bearer tokens are not accepted, so each request is scoped to exactly one organization. Keys are created through the Better Auth api-key endpoints (see @onyx/auth).
Adding an endpoint
Two files change: define the procedure in private/api/src/contract.ts, then implement it in apps/web/src/api/router.ts. The type checker fails until both sides agree.
Define the procedure with oc and Zod, and add it to the exported contract object:
// private/api/src/contract.ts
const listWidgets = oc
.errors(authErrors)
.route({
method: 'GET',
path: '/widgets',
summary: 'List widgets',
tags: ['Widgets'],
})
.output(z.array(widgetSchema));
export const contract = { health, org: getOrg, widgets: listWidgets };Keep the contract file free of relative imports; the OpenAPI script executes it directly under Node.
Then attach a handler. os and the authenticated middleware come from apps/web/src/api/implementer.ts, which already verifies the x-api-key header and puts organizationId on the context:
// apps/web/src/api/router.ts
const listWidgets = os.widgets.use(authenticated).handler(async ({ context }) => {
const db = createDb(env.DB);
// ...query using context.organizationId, return the contract shape
});
export const router = os.router({ health, org: getOrg, widgets: listWidgets });Wire types
Handlers must serialize database rows into the contract shapes:
import type { Organization } from '@onyx/api';
export function serializeOrganization(organization: DbOrganization): Organization {
return {
id: organization.id,
name: organization.name,
slug: organization.slug,
logo: organization.logo ?? null,
createdAt: new Date(organization.createdAt).toISOString(),
};
}OpenAPI
pnpm build in private/api runs scripts/generate-openapi.ts and regenerates openapi.json, an OpenAPI 3.1 document derived from the contract. Run it after changing the contract so the document stays in sync.