packages/api is the auth and billing tier’s HTTP surface. It exports a Hono app that core-api mounts under /api through honoGateway, and the oRPC router that defines the typed procedures the web app calls.
It is a different API from /v1. It has its own routing, its own auth check and its own error format.
Exports
The Hono app
src/index.ts builds the app in this order:
The oRPC handlers receive
{ headers } as their initial context. If neither handler matches, Hono answers its own 404.
The CORS method list has no
PUT, PATCH or DELETE. One procedure, notifications.updatePreference, is declared with method PUT. A cross origin browser call to its REST path would fail preflight. Calls made through the RPC transport use POST and are not affected, and same origin calls through the web app’s proxy involve no CORS at all.Two transports for the same procedures
Every procedure is reachable two ways:- RPC, under
/api/rpc/.... This is what the typed oRPC client in the web app uses. - OpenAPI style REST, under
/api/..., at thepathandmethodeach procedure declares with.route({...}).
openApiHandler adds two plugins:
SmartCoercionPlugin, which coerces query and path strings to the types the Zod schema expects.OpenAPIReferencePlugin, which serves an API reference at/api/docs. Its spec merges the oRPC routes with better-auth’s own OpenAPI schema (auth.api.generateOpenAPISchema()), with the auth paths prefixed by/auth.
Procedure builders
src/orpc/procedures.ts:
There is no studio role in this tier. A procedure that acts on an organization must check membership itself. The billing procedures do it with
requireOrganizationMember and requireBillingManager in modules/payments/lib/billing.ts.
Locale middleware
localeMiddleware in src/orpc/middleware/locale-middleware.ts reads the NEXT_LOCALE cookie from the headers and adds locale to the context, defaulting to the i18n default. createCheckoutLink and createCustomerPortalLink use it.
The router
src/orpc/router.ts:
users
organizations
payments
See Billing and plan limits for the rules.
notifications
admin
All built on the admin procedures in modules/admin/procedures/.
All REST routes are relative to
/api.
Errors
Procedures throwORPCError with a code such as UNAUTHORIZED, FORBIDDEN, BAD_REQUEST or INTERNAL_SERVER_ERROR, and optional data. Billing refusals carry data.reason:
logError('oRPC handler error', error). Responses are in oRPC’s own error format, not the /v1 envelope.
Admin deletion refusals are mapped in modules/admin/lib/deletion-refusal.ts.
Rate limiting and body size
/api is mounted before the /v1 limiter and is not rate limited by Express. Its bodies pass through the global express.json parser first (the gateway forwards the captured raw bytes), so the 24mb limit applies.
Files that look important and are not wired
src/lib/backend-client.tsandsrc/lib/backend-config.ts: a client that signs requests to a separate backend withX-Service-Tokenheaders and a different envelope (success,error). It comes from before the consolidation. Nothing in the package imports the client, and the config module would throw at import ifBACKEND_URL,BACKEND_SERVICE_SECRETorBACKEND_LIFECYCLE_SECRETwere missing.src/lib/openapi-schema.ts:mergeOpenApiSchemas, a helper to merge two OpenAPI documents. The handler builds its spec without it.src/config.ts: an empty config object kept for consistency with the other packages.
Adding a procedure
1
Create the procedure file
Add
src/modules/<area>/procedures/<name>.ts. Start from protectedProcedure unless the call is really public.2
Check authorization yourself
The builder only proves there is a session. Verify the user’s membership of the organization named in the input before reading or writing anything.
3
Register it
Add it to the area’s
router.ts. The key becomes the client method name.4
Rebuild
pnpm --filter @repo/api build. core-api loads the package from dist. The web app gets the new method through the ApiRouterClient type./v1/web, not here. This tier is for things tied to the auth user or the organization’s billing.