Naalya Handbook
Using Chowbea

Using a bus type in the Hub

Import generated bus types in the Education Hub — never hand-mirror API enums or websocket payloads.

The API has exported a type on the bus. You need it in the Education Hub: to type a websocket handler, to switch on InvalidationAction, to use UserType without copying the enum. This page is that import, plus the two patterns already in the tree.

Step 1: Regenerate

API server running, manifest extracted. In the Hub:

bun api:watch
# or one-shot:
bun api:generate

You should see files under src/services/api/_generated/bus/:

_generated/bus/
  index.ts
  libs.shared.src.permission.permission.ts
  libs.shared.src.realtime.realtime.ts
  libs.shared.src.user.user.ts

index.ts re-exports every barrel. Prefer importing from @/services/api/_generated/bus.

If the file is missing, [bus].endpoint is wrong or the well-known route 503'd — Keeping the Hub client in sync.


Step 2: Import

import type {
  RealtimeEventMap,
  InvalidateChange,
} from '@/services/api/_generated/bus'
import { InvalidationAction, Action, Resource, UserType } from '@/services/api/_generated/bus'

Use import type for types. Use a value import for enums — they are runtime objects. Object.values(Action) works. Do not recreate const Action = { READ: 'read', … }.

Never edit the generated file to "fix" a field. Change the API barrel and regenerate.


Websocket payloads

src/lib/realtime/realtime-channels.ts used to hand-write event payloads. It now aliases the bus:

src/lib/realtime/realtime-channels.ts
import type {
  RealtimeEventName as BusRealtimeEventName,
  RealtimeEventMap,
} from '@/services/api/_generated/bus'
import { SubscriptionTokenRequestDtoKind } from '@/services/api/_generated/api.contracts'

export const RealtimeChannel = SubscriptionTokenRequestDtoKind

export type RealtimeEvents = RealtimeEventMap
export type RealtimeEventName = BusRealtimeEventName

Channel kinds still come from OpenAPI (the subscription-token DTO enum) because they are on a REST body. Event payloads come from the bus because they are not. Two pipes, one feature.

Handlers then type as RealtimeEvents['invalidate']Subscribing from the Hub.


Domain enums

Action and Resource are the API's permission vocabulary. The Hub needs the same runtime enum CASL and the UI use. They arrive on the bus; a thin adapter keeps CASL's string-literal unions happy:

src/lib/permissions/permissions.types.ts
import {
  Action as BusAction,
  Resource as BusResource,
  UserType as BusUserType,
} from '@/services/api/_generated/bus'

const Action = BusAction as {
  [K in keyof typeof BusAction]: `${(typeof BusAction)[K]}`
} satisfies Record<string, ActionType>

The satisfies is a tripwire: if the bus enum drifts from what /auth/me rules actually carry, TypeScript fails here — not in a random dropdown. Copy this pattern if you need a bus enum to match a generated REST union; do not start a third copy of the values.

UserType is the same idea for routing (staff vs student vs platform_admin).


Invalidation tables

src/queries/deparment/department.invalidation.ts
import { InvalidationAction } from '@/services/api/_generated/bus'

registerInvalidation(InvalidationAction.DEPARTMENT_CREATE, () => [
  departmentQueryKeys.list,
])

Switch on the enum; do not string-compare 'department.create'. A renamed action becomes a compile error after regen.


What not to do

  • Do not import bus types from api.contracts "because the name looks similar." Contracts are HTTP. Bus is everything else. AuthMeRuleDto['action'] is a REST union; Action is the domain enum.
  • Do not add @ApiExtraModels on the API just to leak a websocket type into Swagger. That is the old workaround this pipe replaced.
  • Do not type a Hub handler as Record<string, unknown> once the bus has the payload. You paid for the extract.

Checklist

  • Symbol exists in _generated/bus after watch.
  • App code imports from @/services/api/_generated/bus (or a one-line re-export like RealtimeEvents).
  • Enums imported as values.
  • No leftover hand-written mirror in src/lib. Delete it in the same PR.

Where to go next

On this page