The boring 80% of a Rust backend and its TypeScript clients, already built and already tested.
Baukit is German for a construction kit, and that is the whole idea. Twenty Rust crates and twenty-two npm packages, each small enough to adopt on its own, plus a CLI that wires them into a working product when you want the whole thing.
Warning
Under heavy development. Expect breaking changes.
Every component is on one 0.x train and minor bumps break API before 1.0. Only the latest minor is supported, and there are no backports. Pull requests are not accepted yet. Issues and discussions are welcome.
Every new service starts with the same three weeks. Graceful shutdown that actually drains. Health endpoints that a load balancer can distinguish from readiness. Config from files and environment with real validation. An error envelope your frontend can branch on. Trace IDs that survive the hop from browser to backend. Prometheus metrics named consistently enough that one dashboard works for every service.
None of it is hard. All of it is fiddly, all of it gets done slightly differently each time, and all of it is what breaks at 3am.
Baukit is that work, extracted from real products, with the tests attached.
Scaffold a product:
cargo install --git https://github.com/PatrickKoss/baukit --tag v0.10.28 --locked baukit-cli
baukit new orders --backend --web
cd orders && make checkAdd --quality strict to generate capability-specific coverage, migration, OpenAPI, browser, image, and native gates. The generated scripts/quality-gate.sh runs the same checks locally. Standard remains the default.
You get a hexagonal Rust backend (orders-domain, -ports, -services, -api, -postgres, -bin), a Vite React app with a typed client generated from the backend's OpenAPI schema, Docker builds, a Helm values file, CI, and conformance tests that fail if the health and metrics contracts drift.
Generated products pull Baukit from crates.io and npm by pinned version, so nothing here needs repository access or an SSH key. Point the CLI at a local checkout with --baukit-path rust when you want path and file: dependencies instead.
Or add one crate to a service you already have:
[dependencies]
baukit-http = "0.1"
baukit-ops = "0.1"The examples/minimal-api notes service is 433 lines of Rust, comments included. Start it:
$ cargo run --manifest-path examples/minimal-api/Cargo.toml
INFO minimal_api: service started api_address="0.0.0.0:8080" operations_address="0.0.0.0:9090" service="minimal-api-api"Two listeners, and that split matters. Your public API is on 8080. Everything an operator needs is on 9090, which you never expose to the internet.
A request comes back with identity and trace context already attached, and you wrote no middleware to do it.
$ curl -i -X POST localhost:8080/notes -d '{"title":"first","body":"hello"}' -H 'content-type: application/json'
HTTP/1.1 201 Created
x-request-id: 11d29641-2e48-471d-83fc-c04de54377ab
traceparent: 00-e82b00b429435b4cda9686a7f66779de-56ded6437f5d3bad-01
access-control-expose-headers: x-request-id,traceparent,tracestate
{"id":1,"title":"first","body":"hello"}Failures use one envelope across every endpoint, with the request ID inside the body so a user can paste it into a bug report and you can find the trace:
$ curl -X POST localhost:8080/notes -d '{"title":"","body":"x"}' -H 'content-type: application/json'
{"error":{"code":"validation_failed","message":"The request is invalid",
"requestId":"0f9bead5-9dd2-4f45-b876-0e12e9d663a3",
"details":{"title":"must not be empty"}}}@baukit/api-runtime parses that shape on the client, so a validation failure arrives in your React code as a typed error with details.title rather than a string you have to guess at.
On the operations port, readiness is a real answer instead of a 200:
$ curl localhost:9090/readyz
{"status":"ready","accepting_traffic":true,
"checks":[{"name":"accepting_traffic","status":"pass","duration_ms":0},
{"name":"state","status":"pass","duration_ms":0}],
"diagnostics":[]}
$ curl localhost:9090/buildinfo
{"service_name":"minimal-api-api","version":"0.1.0","commit":"unknown","rust_version":"1.99.0"}/healthz says the process is alive, /readyz says it should receive traffic, and they are genuinely different during a deploy. When shutdown starts, the traffic gate flips /readyz to failing while in-flight requests finish draining, which is the difference between a rolling deploy and a handful of dropped connections.
Metrics come out already named the way the shipped Grafana dashboards expect: http_requests_total, http_request_duration_seconds, http_requests_in_flight, build_info. A linter in CI keeps them that way.
Nothing here depends on the CLI, and no crate drags in the rest. Take the error envelope and skip the job queue.
| Crate | What it gives you |
|---|---|
baukit-runtime |
Process lifecycle, graceful drain, supervised background tasks, listener composition |
baukit-config |
Layered file and environment config, validation, secret wrappers that refuse to print |
baukit-http |
Axum middleware, the error envelope, upstream retry classification, CORS, default cache policy |
baukit-ops |
Separate liveness, readiness, metrics, and build-info endpoints |
baukit-telemetry |
Structured logs, OpenTelemetry traces, Prometheus metrics from one builder |
baukit-auth |
OIDC, Clerk, and WorkOS verification; personal access tokens; Axum principal extraction |
baukit-mcp |
OAuth-protected Streamable HTTP, scoped tool ports, resource metadata, body limits, and quotas |
baukit-jobs |
Durable PostgreSQL outbox and supervised workers |
baukit-openapi |
Utoipa metadata, deterministic schema output, drift checks |
baukit-ratelimit |
Redis token buckets keyed by identity or client IP |
baukit-sync |
Per-owner revision allocation for offline clients |
baukit-events |
Versioned event envelope with stable validation codes |
baukit-integrations |
Connector contract for cursor-paged imports, verified webhooks, health |
baukit-credential-vault |
Versioned AES-256-GCM encryption behind a storage-neutral port |
baukit-push |
Provider-neutral push delivery with an Expo adapter, a device registry, and daily delivery claims |
baukit-egress |
Outbound HTTP client for user-supplied URLs that pins DNS answers to public addresses |
baukit-erasure |
Transactional profile erasure, keyed receipts and fences, durable identity account deletion |
baukit-core |
Dependency-light vocabulary shared by the others, plus optional keyset pagination |
baukit-suite |
Peer linking, signed event delivery and transactional ingestion |
baukit-test |
Docker PostgreSQL and Redis fixtures, a mock OIDC issuer, conformance suites |
| Package | What it gives you |
|---|---|
@baukit/api-runtime |
Auth and request-ID headers, trace propagation, normalized errors, safe retries |
@baukit/auth-web · -native |
OIDC PKCE, Clerk and WorkOS adapters, refresh rotation |
@baukit/auth-node |
OIDC device authorization with S256 PKCE, bounded polling, refresh rotation, and a locked profile cache |
@baukit/data-contracts |
Storage contracts plus conformance suites you run against your adapter |
@baukit/data-contracts-dexie · -expo-sqlite |
IndexedDB and Expo SQLite adapters, verified in a real browser and on a real Android device in CI |
@baukit/integrations-client |
Connection health, OAuth session coordination, provider registry |
@baukit/suite-client |
Suite linking, authorization and connected-app state for web and native clients |
@baukit/sync-client |
Sync scheduling, transport, status store, push-batch ordering |
@baukit/ui-tokens |
Design-token schema, contrast checker, CSS and React Native compilers, a no-raw-color eslint rule |
@baukit/a11y-core |
Focus traps, inert backgrounds, announcements, reduced motion, on both web and native |
@baukit/navigation |
Collapsible navigation for React DOM and React Native with product-owned routes and tokens |
@baukit/analytics-core |
Typed events with consent and privacy controls, PostHog transports for web and native |
@baukit/localization-core |
Locale resolution, formatting policy, timezone-safe civil-date math |
@baukit/notifications-core · -expo |
Local notification planning across DST and time zones, and replacement that touches only the requests one namespace owns |
@baukit/preferences-core |
Identity-guarded preference controller |
@baukit/pwa-web |
Request classification and cache strategies for a service worker you own |
@baukit/events |
Zod schemas for the same envelope baukit-events speaks |
Every package ships ES modules. Each export lists import and a matching default condition, so bundlers, Node, and Jest resolve @baukit/* without a module map; Jest still needs to transform them. The ./vitest test subpaths list only import, because Vitest itself is ESM-only.
The Rust and TypeScript halves are written against the same contracts, and in the case of the event envelope both test suites read the same fixtures/events/event-envelope-v1.json. Drift between the browser and the server shows up as a failing test rather than a bad payload in production.
The generated product isn't a snapshot that rots. CI scaffolds a fresh one on every push and runs cargo fmt --check, clippy -D warnings, the full test suite, and an OpenAPI drift check against it, in backend, web, and combined flavors. Template code is linted exactly like committed code.
The rest of the gate is similar. MSRV stays pinned at Rust 1.95 and CI checks it separately. cargo deny runs on advisories and licenses. A metric-name linter keeps dashboards and alerts honest. Dexie conformance runs in a real browser and the Expo SQLite adapter runs on a real Android device. Nobody skips the Docker-backed integration tests.
The repo also ships a Helm chart and Grafana dashboards, alerts, and recording rules matched to the metric names above, plus an nginx njs verifier for signed media grants that checks the same vectors as baukit-core's signer. The agent skills teach Claude Code and Codex the conventions for product work. The QA audit runs as /qa in Claude Code or $qa in Codex.
See remote MCP for tool adapters, client setup, and the OAuth flow. Existing products follow the MCP migration guide.
Contracts and recipes live in docs/platform/: local-data ownership, offline readiness, integration reliability, accessibility, localization, native quality gates, telemetry, onboarding, calendar export, and mirrored Rust/TypeScript domain logic. Release process is in docs/releasing.md.
Three workspaces, no root workspace, so always pass a manifest path.
rust/ 19 library crates
typescript/ 21 packages, pnpm + Turborepo
cli/ the baukit binary
templates/ what the CLI renders
examples/ minimal-api, expo-sqlite-conformance
make toolchain # mise: Node 26, Temurin 25.0.4.1+1, Rust 1.99.0
mise exec -- make ci # fmt, clippy, tests, and the TypeScript gatesMise installs Corepack 0.36.0 with Node. Corepack reads the pnpm 12.9.1 pin in typescript/package.json. Docker and the Android SDK stay system dependencies. iOS work needs xtool with a Darwin SDK you generate from a manually downloaded Xcode.xip, and Linux has no iOS simulator.
The @baukit/* packages publish to npm and the eighteen library crates publish to crates.io. The CLI installs from a Git tag. Crates, packages, and templates move together under one vX.Y.Z tag.
MIT. See LICENSE.