OSS-first docs
These docs teach the open system first: contracts, generated surfaces, runtimes, governance, and incremental adoption. Studio shows up as the operating layer on top, not as the source of truth.
Database setup
Set up Drizzle without coupling modules to your database
Use Drizzle as the PostgreSQL migration and query layer while keeping ContractSpec modules portable. Compose schemas from module contributions, apply migrations explicitly, seed deterministic fixtures, and bind AuthOS, BillingOS, and RBAC through host-owned adapters.
Install the database boundary
bun add drizzle-orm drizzle-kit @lssm-tech/lib.schema @lssm-tech/integration.provider-database @lssm-tech/lib.identity-rbacDrizzle owns SQL schema and migrations. ContractSpec owns portable entities, domain contracts, RBAC policy semantics, and governed database operation envelopes.
identity-rbac
Provides User, Session, Organization, Team, Role, Permission, PolicyBinding, API key, and passkey entities through identityRbacSchemaContribution.
packages/libs/identity-rbac/src/entities@lssm-tech/lib.identity-rbac/entities
AuthOS
Treat AuthOS as the UI/runtime adapter layer. Persist identities, sessions, organizations, teams, and RBAC through identity-rbac tables and auth adapters.
packages/modules/auth-os@lssm-tech/module.auth-os
BillingOS
Keep BillingOS country-neutral and provider-free. Implement search, quote, invoice, payment, and tax ports with database-backed adapters in the host app.
packages/modules/billing-os/src/ports@lssm-tech/module.billing-os/ports
Recommended setup flow
Compose database entities from module schema contributions before writing Drizzle tables by hand.
Generate or maintain a Drizzle PostgreSQL schema from the composed model, then keep the Drizzle schema as migration input.
Use drizzle-kit generate for migration files, drizzle-kit migrate for applying them, and custom migrations for RLS, seed prerequisites, or unsupported DDL.
Bind runtime reads and writes through @lssm-tech/integration.provider-database instead of importing Drizzle into modules such as AuthOS or BillingOS.
Seed tenants, users, roles, permissions, and minimal domain fixtures in deterministic order after migrations and before UI demos/tests.
Fail closed on RBAC data access: tenant id, actor id, policy decision, idempotency key, audit refs, and expected write sets must be explicit for governed writes.
Drizzle configuration
Keep drizzle.config.ts, src/db/schema.ts, and the drizzle/ migration folder in the host app or deployment package. Do not place provider credentials or migration side effects inside reusable modules.
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./src/db/schema.ts",
out: "./drizzle",
dbCredentials: { url: process.env.DATABASE_URL! },
strict: true,
});Compose module schemas
Start with reusable contributions such as identityRbacSchemaContribution, then add host-owned entities for AuthOS adapters and BillingOS persistence. Generate Drizzle tables from that composed source or keep a reviewed generated schema checked in.
import { composeModuleDatabaseSchemas } from "@lssm-tech/lib.schema";
import { identityRbacSchemaContribution } from "@lssm-tech/lib.identity-rbac/entities";
const schemaSource = composeModuleDatabaseSchemas([
identityRbacSchemaContribution,
authHostSchemaContribution,
billingHostSchemaContribution,
]);
await writeFile("src/db/schema.ts", schemaSource);Bind modules through providers and ports
Use @lssm-tech/integration.provider-database for tenant-scoped providers, governed reads, and governed mutations. AuthOS and BillingOS should receive host adapters, not raw Drizzle clients.
import { createTenantScopedDatabaseProvider } from "@lssm-tech/integration.provider-database/impls";
const tenantDb = createTenantScopedDatabaseProvider({
provider: postgresDatabaseProvider,
tenantId: session.tenantId,
});
export const billingPorts = {
searchCustomers: (query) => customerRepository.search(tenantDb, query),
searchSellableItems: (query) => priceBookRepository.search(tenantDb, query),
};Adopt the unified database runtime
For new surfaces, prefer createDatabaseRuntime() from @lssm-tech/integration.runtime-managed/database. It binds pooling, observability, RLS tenant scoping, AuthOS principal carriage, and pre-bound governed query/mutation resolvers into a single adopt-once handle — so consumers get the safe path with nothing extra to configure.
Connection pooling (transaction/session/direct modes; transaction-pooler sets prepare:false for pgBouncer/Supabase).
Zero-config observability: every governed read/write emits an OTel span + duration metric via the database.query / database.mutation SignalSpecs — nothing to wire per call.
Automatic RLS tenant scoping: withTenant(tenantId) issues set_config('app.tenant_id', …, true) tx-local before every statement, and fails closed when no tenant is resolvable.
AuthOS principal carriage: TransportContext.principal is forwarded unstripped; policy decisions stay in identity-rbac handlers (the runtime never bypasses the PDP).
Typed, N+1-safe relational reads: parent→children resolve in one governed JOIN statement (database.query.relational), never an ungoverned raw query.
// server-only — import from the ./database subpath, never the package root
import { createDatabaseRuntime } from "@lssm-tech/integration.runtime-managed/database";
import { createInstrumentation } from "@lssm-tech/lib.observability";
const runtime = createDatabaseRuntime({
databaseUrl: process.env.DATABASE_URL!,
pooler: "transaction", // prepare:false, sized for pgBouncer/Supabase
instrumentation: createInstrumentation({ serviceName: "companyos" }),
});
// known-tenant server path: RLS app.tenant_id is set automatically, fail-closed
const tenantDb = runtime.withTenant(session.tenantId);
const page = await tenantDb.governedQuery(workspacesDescriptor, { principal: session.userId });
// page.data + page.pageInfo.cursor / hasNextPage (cursor pagination by default)createDatabaseRuntime pulls node-only postgres (net/tls/perf_hooks). Import it ONLY from the server-only @lssm-tech/integration.runtime-managed/database subpath in RSC, route handlers, or server modules — never from the package root barrel, which client components import.
Stream reads to the client (SSR bridge)
Prefetch a governed query in a Server Component, dehydrate it, and hydrate the existing client data engine so the first client read is a cache hit with no second fetch. Loading, empty, and error states are driven by the shared query engine.
// RSC (server): prefetch through the governed resolver, dehydrate the cache
import { prefetchGovernedQuery, dehydrateGovernedQuery } from "@lssm-tech/lib.presentation-runtime-next/data";
const result = await prefetchGovernedQuery(resolver, envelope, { principal: userId });
const dehydrated = dehydrateGovernedQuery(result, envelope); // stamps a future staleAt
// Client island ('use client'): seed the SAME CacheStore → zero second fetch
// <HydrationBoundary state={dehydrated}> wraps your ContractDataEngineProvider.
// Build the engine with createDataEngine({ localOffline: 'offline-capable', advanced: { cacheStore } }).Seed in dependency order
Tenant/workspace rows and environment metadata.
Identity rows: users, accounts, sessions, organizations, members, teams.
RBAC rows: roles, permissions, policy bindings, and deny fixtures.
Domain fixtures: BillingOS customers, price books, quotes, invoices, and payment evidence.
Audit and replay rows used by governed mutation tests.
Set up i18n
Use ContractSpec translation specs, the translation runtime, SSR snapshots, mobile locale ownership, and optional i18next projection from one reusable i18n layer.
Flexible import templates
Publish a recommended data-exchange import shape while users import CSV, JSON, or XML files with alternate headers and localized values.
Why ContractSpec
Keep educational and comparison content reachable without letting it define the primary OSS learning path.