Authoring Bus Types
The two ways to put a type on the bus, and the rules the extractor enforces.
Everything here happens in your API repo. Two ways to mark a type; use either or both.
Channel 1 — *.chowbea.ts barrels
Every exported type in a file matching *.chowbea.ts rides the bus. Intended as colocated barrels, so types live next to the service that owns them instead of migrating to some shared/ graveyard.
// Re-export types that already exist elsewhere…
export type { Grade, StudentId } from './models';
// …or declare bus-specific ones here
export type GradeMap = Map<StudentId, Grade>;Re-exports are resolved to their original declaration — the manifest carries export type Grade = … from models.ts, not the re-export line. Your frontend gets the real thing.
The barrel's path (relative to your tsconfig rootDir, minus the suffix) becomes the barrel key, which decides the generated filename on the frontend: src/exams/grade.chowbea.ts → key exams/grade → _generated/bus/exams.grade.ts.
Channel 2 — @chowbea-export
Tag a single declaration anywhere, no new file required:
/** @chowbea-export */
export interface Plan {
name: string;
seats: number;
}
export const PLAN_LIMITS = { max: 50 }; // not marked — stays homeMarked types group under a reserved _marked/<dir> key (src/billing/plans.ts → _marked/billing).
Prefer barrels for anything a domain owns as a set; use the tag for one-offs you don't want to re-home. Mixing them is fine — the rules below apply identically to both.
Naming the generated file
By default the generated filename follows the source path: src/exams/grade.chowbea.ts → _generated/bus/exams.grade.ts. When you'd rather choose, decorate the file with chowbea-name:
/** chowbea-name "grades" */
export type { Grade, StudentId } from './models';
export type GradeMap = Map<StudentId, Grade>;The frontend now emits _generated/bus/grades.ts. The @ prefix is optional — @chowbea-name "grades" works identically — and the tag can sit in any comment, block or line.
It works on marker-channel files too, lifting those types out of the _marked/ grouping into a file of their own:
/** chowbea-name "billing" */
/** @chowbea-export */
export interface Plan { name: string; seats: number }A name may contain / for grouping — "domain/grades" emits domain.grades.ts, the same flattening derived keys get.
First one wins. If a file declares chowbea-name more than once, the topmost occurrence is used and the rest are ignored — a file is never ambiguously named.
Names are validated at extraction: they must be plain (no leading slash, no .. segments, no backslashes), index and the _marked/ namespace are reserved, and two files cannot resolve to the same output — whether both claimed the name or one collided with another file's derived key. Silent merging of two files into one output is exactly the failure this rule exists to prevent.
The rules
Every violation is an extraction-time error with a file:line, and they're reported in one batch — you get the full list in one run, not one error per re-run.
Types only
Type aliases, interfaces, and enums. That's it.
export type Grade = 'A' | 'B'; // ✅
export interface Plan { seats: number } // ✅
export enum Level { Low = 'low' } // ✅
export const LIMIT = 10; // ❌ error: only types ride the bus in v1
export class Service {} // ❌
export function calc() {} // ❌Types erase at compile time, so shipping their text is inert. A class or function body is executable code — carrying it would mean a compromised endpoint could inject code into your frontend bundle, and the body would reference backend-only things (process.env, your DB client, node: APIs) that don't exist in a browser anyway. If you need shared logic, publish a shared package.
Enums are the deliberate exception: they're values too, but their value space is small enough to validate exhaustively (literal members only, no computed names, no call expressions).
The closed-world rule
A bus type may reference:
- primitives and TypeScript lib built-ins —
Map,Set,Date,Record,Partial, generics, unions, mapped/conditional/template-literal types - other bus types
Anything else is an error:
import type { Grade } from './models'; // Grade is NOT on the bus
export type GradeMap = Map<string, Grade>;src/bus.chowbea.ts:3 "GradeMap" references "Grade" (src/exams/models.ts:12) which is not
on the bus — add it to a .chowbea.ts barrel or mark it @chowbea-export.The error tells you exactly what to add and where it lives, so resolving the closure is a copy-paste rather than an investigation. This is deliberately strict: automatic transitive closure would silently drag half your domain — and its node_modules types — into your frontend the first time someone references a Prisma model.
References to types from node_modules are rejected outright in v1.
No duplicate names
A type name may be registered once across the entire bus — one barrel, one tag, one name. Register Grade in two barrels and you get both sites named in the error:
src/exams/grade.chowbea.ts:1 "Grade" is registered twice on the bus — at
src/exams/grade.chowbea.ts:1 and src/legacy/old.chowbea.ts:4. Remove one.Names are flat on the wire, so a collision would otherwise mean one type silently winning. Silent winners are how type buses rot.
Reserved keys
_marked is reserved for the tag channel, and index is reserved because the frontend emits an index.ts barrel. A *.chowbea.ts file that would produce either key is an error. Files outside your tsconfig rootDir are rejected too — they'd produce keys with .. segments.
Known limitation
Qualifier-less typeof import('./x.js') module-namespace types are not yet validated by the closed-world check. typeof import('./x.js').foo is caught. Tracked for a future release.
What good marking looks like
src/
├── exams/
│ ├── models.ts # ordinary types, not on the bus
│ ├── grade.chowbea.ts # barrel: re-exports Grade, StudentId; declares GradeMap
│ └── exams.service.ts
├── realtime/
│ └── channels.chowbea.ts # barrel: channel kind unions
└── billing/
└── plans.ts # one interface tagged @chowbea-exportOne barrel per domain that shares types, tags for strays. Resist the single mega-barrel — barrel keys become frontend filenames, and _generated/bus/everything.ts helps nobody.