Package boundaries
Baseleg is a modular monolith with an explicit dependency direction.
Allowed dependencies
apps/*→packages/application/*,packages/ui, and selectedpackages/domain/*(types/value objects only)packages/application/*→packages/domain/*,packages/shared/*packages/infrastructure/*→packages/domain/*,packages/shared/*, andpackages/application/*type-only, to implement that layer’s repository/port interfaces (e.g.PersonRepositoryD1 implements PersonRepository) — this is the standard ports-and-adapters direction: the port is defined where it’s consumed (application), the adapter implements it. Never a runtime/value import — onlyimport type.packages/domain/*→packages/shared/*packages/ui→packages/shared/*
Forbidden dependencies
- Domain must not import infrastructure.
- UI must not import infrastructure, outside the composition-root exceptions below.
- Application must not import infrastructure implementations directly (use ports + wiring).
- Astro pages/routes must not import Drizzle schema or repository implementations, outside the composition-root exceptions below.
- Shared must not depend on domain/application/infrastructure/ui.
- Infrastructure must not import runtime values from application — type-only port imports are the one allowed exception (see above).
Composition-root exceptions
A small, fixed set of files legitimately wire infrastructure into the app — this is where
dependency injection actually happens, so it’s the one place apps/web is expected to know
about concrete infrastructure. Enforced today via the exemption list in
eslint.config.mjs; listed here so the two stay in sync:
apps/web/src/middleware.ts— constructs the DB connection and repository implementations, puts them onAstro.locals.apps/web/src/lib/auth.ts— wires the auth infrastructure adapter.apps/web/src/pages/api/logout.ts— auth session wiring for logout.apps/web/src/env.d.ts— typesApp.Localsagainst the concrete typesmiddleware.tsactually puts there; this is a type-only reflection of the composition root, not a new one.
Nothing outside this list may import infrastructure from apps/web. If you find yourself
adding a new file to this list, that’s worth a second look — most pages should only ever
need Astro.locals.repos/Astro.locals.db as already-wired values, or an application-layer
use case, never a fresh infrastructure import of their own.
Good imports
// apps/web: call a use case
import { createPerson } from '@baseleg/application-people';
// application: use domain + shared
import type { PersonId } from '@baseleg/domain-people';
import { Result } from '@baseleg/shared-result';
// infrastructure: implement a repository using domain types
import type { PersonId } from '@baseleg/domain-people';
// infrastructure: implement an application-layer port (type-only)
import type { PersonRepository, ListPeopleFilter } from '@baseleg/application-people';
export class PersonRepositoryD1 implements PersonRepository { /* ... */ }
Bad imports
// domain must not import infrastructure
import { db } from '@baseleg/infrastructure-db';
// infrastructure must not import a runtime value from application — type-only only
import { registerPerson } from '@baseleg/application-people';
// UI/routes must not talk directly to DB/schema
import { peopleTable } from '../../../db/schema';
// UI must not import repository implementations
import { PeopleRepositoryD1 } from '@baseleg/infrastructure-repositories';