Skip to content

Overview ​

@saflib/monorepo provides npm workspace conventions, shared package scaffolding, and tooling for SAF monorepos. It focuses on npm packages — how workspaces are organized, how package.json surfaces are defined, and how new packages are added.

For TypeScript — composite project references, cross-package typing conventions, and static import-graph analysis — see @saflib/imports.

Use this package's saf-monorepo command for formatting and lockfile management, and the add-package, add-export workflows to add standalone packages.

What this package provides ​

Init workflows (express/init, sdk/init, drizzle/init, etc.) and monorepo/add-package scaffold packages with these defaults. Golden stubs live under saflib/base and workflow template trees.

Product layout ​

See base. A SAF product is a tree of npm workspace packages. In a multi-product repo each product lives under {product}/; a single-product repo may place clients/, service/, and dev/ at the repo root instead.

Typical top-level layout:

{repo}/
├── {product}/
│   ├── clients/          # Vue SPAs, shared client code, links
│   ├── dev/              # docker compose for local development
│   ├── notes/            # specs and planning docs
│   ├── service/          # backend packages (see below)
│   └── {offshoot}/       # optional bounded feature slice (db, http, spec, sdk, test)
├── deploy/               # production images and deploy scripts
├── saflib/               # SAF platform submodule
├── package.json
└── package-lock.json

Use product/init to copy saflib/base into a new product. Workflows extend saflib/base in place so platform and product stay aligned.

clients/ ​

Web clients only (Vue SPAs today). Each SPA is its own workspace package named after its subdomain (app/, auth/, …), built together from a build/ package. Shared client logic lives in common/; cross-SPA link objects live in links/ so server code does not depend on Vue. See @saflib/vue.

service/ ​

Backend packages for one product. Terminology:

  • Library — shared logic, no long-running process
  • Server — listens on a port (HTTP, gRPC, …)
  • Worker — long-running work without a public port (cron, jobs runtime)
  • Service — a coordinated set of servers and workers (e.g. identity)
  • Monolith — runs the product's servers and workers together
PackageRoleKind
commonshared service wiring (loggers, context, deps)library
specOpenAPI contractlibrary
dbdrizzle schema and querieslibrary
httpExpress server and route handlersserver
sdkTanStack Query clients tied to the API contractlibrary
cronscheduled job enqueuerworker
jobsasync job runtime and trigger mapworker
integrations/third-party API wrappers (see below)integration
monolithboots http, cron, jobs, and related runtimesmonolith
testshared test factories for spec modelstest

Dependency flow: spec and db are independent layers. http depends on both and translates between wire and storage models. sdk depends on spec so clients stay decoupled from storage. monolith composes the runnable backends. All clients/* packages depend on sdk for API access.

Offshoots — {product}/{offshoot}/ holds a vertical slice (db, http, spec, sdk, test) when a feature is large enough to warrant its own OpenAPI surface and packages but still mounts into the parent product's HTTP app. See @saflib/openapi.

Integrations — {product}/service/integrations/{vendor}/ wraps a third-party SDK behind mockable calls (e.g. stripe, sendgrid). See @saflib/integrations.

dev/ and deploy/ ​

dev/ runs the product locally (Caddy, Vite clients, monolith, Azurite, Kratos, …). deploy/ holds production Docker and deploy scripts when products share one infrastructure.

Package naming ​

Packages under {product}/service/{name}/ import as @{org}/{product}-{name}:

  • acme/service/db/ → @acme/acme-db
  • acme/service/http/ → @acme/acme-http

Offshoot packages add the offshoot name: acme/dossier/spec/ → @acme/acme-dossier-spec.

Client packages follow the same pattern: acme/clients/app/ → @acme/acme-app.

npm package surface ​

Each workspace package exposes an explicit public surface in package.json:

  • exports — subpath map consumers import (@scope/pkg/handlers/foo, not package root). Use single-star folder globs (./handlers/* → ./handlers/*.ts) plus explicit entries for allowlisted root files. No package-root catch-all.
  • imports — package-local # specifiers mirroring the same folders (#handlers/*, #errors.ts). Same-package code uses #… with file extensions, not ../ climbs into sibling folders.
  • saf.kind — declares package role (db, http, spec, spa, sdk, lib, integration, test, …). Layout tooling infers kind from identifier deps when omitted.
  • sideEffects — false unless the package has explicit browser/CSS entry exceptions.

monorepo/add-export adds a module and updates exports/imports via glob patterns. To validate export coverage and layout rules, use saf-analyze-package.

Layout rules: production .ts/.tsx files belong in thematic folders, not loose at the package root, except for allowlisted entrypoints (index.ts, main.ts, run.ts, config files, …). @saflib/dev-tools saf-analyze-package reports layout, dead-code, and oversized-file issues.

TypeScript in SAF monorepos ​

TypeScript project references, composite builds, and import-graph tooling live in @saflib/imports:

Each package still extends @saflib/monorepo/tsconfig.json (or @saflib/vue presets for SPAs) as part of npm scaffolding; @saflib/imports owns the reference graph and enforcement.

Deployment models ​

Products can run standalone or as a shared hub (one domain, one identity, one monolith). Both are supported.

AspectStandalone productProduct hub
DomainOne per productOne for all (subdomains per product)
IdentityPer-productSingle (hub)
Auth UIPer-product auth subdomainSingle auth.{domain}
Deploy monolithsOne container per productOne container (hub monolith)
Link subdomainsProduct-specific (app, …)Product-prefixed (app.recipes, …)

Use hub dev/deploy for a single login and deployment unit; keep per-product dev/ for focused standalone development.

The saflib/ submodule ​

The SAF source lives in a git submodule so products can reference, edit, and typecheck it directly. Changes under saflib/ are tracked in the submodule; the parent repo records only the checked-out commit.

Testing and typecheck ​

Run tests from the package directory:

bash
cd {product}/service/http && npm run test
cd {product}/service/http && npx vitest run -- routes/scans/execute

Run npm run typecheck from the repo root for incremental solution builds. After adding packages or changing workspace deps, run npm run tsconfig:sync and npm exec saf-imports tsconfig check.

Package-specific testing guidance lives in the relevant saflib/ package docs (e.g. @saflib/drizzle queries).