Core Composition
Byline is a framework, not a single application, so how its packages compose matters as much as what each one does. This document covers the composition machinery — the dependency-injection container, the server-side entry point, the field-level adapter slots, and the command wrapper — and the architectural rules that keep the package boundaries clean.
The composition machinery
- Registry / AsyncRegistry — a typed DI container (
packages/core/src/lib/registry.ts) with compile-time dependency-graph validation via TypeScript conditional types. - initBylineCore() — the server-side entry point. It composes
config,collections,db,storage,logger, and theAdminStoreaggregate into aBylineCoreinstance. Server-side callers retrieve the resolved core viagetBylineCore<AdminStore>(). - Admin modules in @byline/admin — each module ships as
commands.ts+repository.ts+service.ts+dto.ts+schemas.ts+errors.ts+abilities.ts. Repositories are plugged intoAdminStorefrom@byline/db-postgres/admin.
This keeps the package boundaries decoupled — important when the product is a framework rather than a single app — and avoids speculative abstraction.
Field-level server adapter slots — ServerConfig.fields.*
An adapter package can register a server-side function alongside its client-side React component, via mirrored slots on ClientConfig.fields.* and ServerConfig.fields.*. The richtext adapter is the reference user of this pattern:
- Client side —
ClientConfig.fields.richText.editor: RichTextEditorComponent. A render-only React component, registered vialexicalEditor()from@byline/richtext-lexical. - Server side, write path —
ServerConfig.fields.richText.embed: RichTextEmbedFn. A pure function called once per rich-text leaf the write path discovers in an outgoing document, registered vialexicalEditorEmbedServer()from@byline/richtext-lexical/server. - Server side, read path —
ServerConfig.fields.richText.populate: RichTextPopulateFn. The mirror of the embed function; called once per rich-text leaf the read pipeline discovers in a returned document, registered vialexicalEditorPopulateServer().
Two consequences shape any future field-level adapter:
- The subpath split is the right shape. An adapter package with both client and server pieces ships two entry points so consumers of one don't bundle the other.
@byline/richtext-lexical(UI) and@byline/richtext-lexical/serverare the reference example. - The framework owns the walker. Field-level server adapters receive context per leaf (
{ value, fieldPath, collectionPath, readContext }) — they don't walk the document tree themselves.collectRichTextLeaves(packages/core/src/services/richtext-populate.ts) is the per-field-type walker; a future adapter gets its own walker but slots into the same place in the read pipeline (between relation populate and user-landafterRead).
createCommand — a uniform command shape
Every admin operation in @byline/admin is declared with createCommand (packages/admin/src/lib/create-command.ts), which folds the four standard steps — validate input → authorise → invoke → shape output — into a single specification:
export const listAdminUsersCommand = createCommand({ method: 'listAdminUsers', auth: { ability: ADMIN_USERS_ABILITIES.read }, schemas: { input: listAdminUsersRequestSchema, output: adminUserListResponseSchema }, handler: ({ input, deps }) => new AdminUsersService({ repo: deps.store.adminUsers }).listUsers(input),})The wrapper runs the four steps in fixed order — Zod-parse input → resolve admin actor → invoke handler → Zod-parse output — and returns a function with the (context, input, deps) => Promise<Output> signature the server fns and tests already expect.
The auth slot is a discriminated union with two variants:
{ ability: 'admin.users.read' }— a full admin gate. Delegates toassertAdminActor, which requires anAdminAuthactor holding the named ability, and inherits the super-admin bypass fromAdminAuth.assertAbility.{ authenticated: true }— an identity gate only. Delegates torequireAdminActorwith no ability check. Used byadmin-accountself-service commands, where the security property is "you may only mutate your own row," enforced structurally by sourcing the target id fromactor.id.
The handler slot takes an args object — { context, input, deps, actor } — so a handler cherry-picks what it needs without positional ordering. actor is already narrowed to AdminAuth by the auth step, so commands that perform self-checks (e.g. disableAdminUser, deleteAdminUser) read it directly.
Document-collection operations (create / update / delete / status / upload) are a separate enforcement path: they are gated by assertActorCanPerform inside the document-lifecycle services in @byline/core, and do not flow through createCommand. See Authentication & Authorization.
Architectural guard rails
Three rules hold the line across the codebase:
- Feature wiring lives in feature packages, not in @byline/core. Byline composes; it does not own. A feature package ships its own composition factory; the integrating app wires it in.
@byline/coreprovides theRegistryprimitive and theinitBylineCorecomposition point but does not import feature packages directly. - Auth keys, not auth realms.
createCommandtakes an ability expression, not an enumeratedmode. TheAbilityRegistryis the source of truth — collections and plugins contribute their abilities at registration time, and the wrapper stays open to whatever they declare. - The adapter boundary is permanent.
IDbAdapter/IStorageProvider/SessionProviderare the contracts adapter packages implement. Feature code consumes them via interface; it never wires concrete dependencies (Drizzle pools, argon2, S3 clients) directly. This is what keeps "swap the adapter" a single-file change inbyline/server.config.ts. Transactions live behind this boundary too: the request-scopedwithTransactioncapability is an adapter concern — the machinery sits in@byline/db-postgres, core only declares the optionalIDbAdapter.withTransactioncapability, and a non-transactional (e.g. HTTP-gateway serverless) adapter must reject it loudly rather than degrade silently. See Transactions.
Code map
Concern | Location |
|
|
|
|
|
|
Admin module commands |
|
|
|
|
|
|
|
|
|
Admin request-context resolver |
|