2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00
2026-11-14 09:56:45 +00:00

Example App

A multi-tenant application on ConusAI Cloud, generated from the ConusAI App Template. Sign-in, organizations, workspaces and projects with roles and invitations, audit, usage metering, observability, background jobs, i18n and an accessible branded UI are already here. What you write is domain modules under modules/.

Agents and new developers: read AGENTS.md first. Claude Code reads it through CLAUDE.md, with the committed permissions, format-on-edit hook and the support-report skill in .claude/; Codex and OpenCode read AGENTS.md directly.

Run it locally

pnpm install
pnpm dev

That is the whole of it, and it needs no .env. pnpm dev (scripts/dev.mjs) starts, on this machine:

  1. Postgres in Docker, with the same three roles a linked managed database exposes (compose.dev.yaml, dev/postgres-roles.sql), on 127.0.0.1:55433;
  2. the fake platform of @conusai-kit/testing on 127.0.0.1:4010: Blob, OCR, the AI gateway, audit and usage, in memory. A structured model request is answered with the smallest value its schema accepts, so what you build on the model runs to its end;
  3. the migrate job, then the demo data (pnpm seed, below);
  4. the worker and the web shell (vite dev) on http://127.0.0.1:3000.

It prints how to sign in as each demo person and stops everything but Postgres on Ctrl+C (docker compose -f compose.yaml -f compose.dev.yaml down stops that too; add -v to drop the data). A value you want different is a variable in your shell, not a file: DEV_WEB_PORT=3200 pnpm dev; the three DEV_*_PORT variables move the ports, and POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL (all three) use a Postgres of your own. pnpm dev:web runs the shell alone against a stack that is already up.

Workspace and project creation refresh TanStack loaders and close the completed panel without remounting the frame or interrupting an Agent draft.

Document review, export, and confirmation actions stay disabled until their browser handlers are attached, including after a decision reloads the current server state.

Chat, Support, and Guides

The default Postgres starter selects agent, guides, and support. The Agent action is in the frame on workspace and project pages. It streams through TanStack AI and the ConusAI gateway, stores conversations in the app database, and keeps the AI disclosure and approval rules. Help contains Guides and Support in the same side panel; phones use a full-screen sheet. The Notes tour starts only when requested from Help or the setup checklist. After privacy choices are resolved, the Agent keeps its header in view while the conversation scrolls and the composer receives focus.

pnpm dev uses a simulated gateway reply and runs the code guides locally. Support shows that it is not connected: real conversations require a configured CONUSAI_SUPPORT_CHANNEL and Cloud service permissions. Selection requests scopes; it does not grant them or establish service readiness. The Postgres starter declares /app/.conusai/guides-bundle.json for native code-guide publication from the image (ADR-045); Cloud publication permissions and review still apply. Production verifies the native service contract and release identity before provider calls.

Keep UI code on the kit: TanStack Start owns routes, @conusai-kit/ui/data owns TanStack DB collections and writes, and @conusai-kit/agent owns the durable TanStack AI conversation. Do not add a second chat store or provider SDK. The Agent currently requires the Postgres profile; alternate profiles retain the support limits documented in profiles/ and modules/agent/README.md. The module record retains custody after an included module is turned off; source/profile pruning refuses adopted modules rather than deleting their data ownership.

The same stack as containers, for a closer look at what is deployed:

cp .env.example .env      # then set AUTH_SESSION_SECRET
docker compose -f compose.yaml -f compose.dev.yaml up --build

After deploying a built image, verify the public shell in Chromium:

pnpm exec playwright install chromium
node scripts/verify-shell.mjs https://your-app.example.test

This checks that theme and English/Lithuanian controls work in both directions and survive reloads. The image serves content-hashed assets at /assets, and the build fixes their addresses: the Start build ignores CONUSAI_ASSET_PREFIX. Its compiled chunk URLs must agree with the URLs the server renders, or the page can appear complete while its controls never become interactive. Update and redeploy the image to apply a template fix; recreating the project or its database is unnecessary.

The demo data

pnpm seed (apps/worker/src/seed.ts) writes it, with the kit's own use cases and nothing else: people come into being as they do at sign-in, and everything after that is a procedure called as the person who may call it, so the permission checks, the invitation flow and the tenancy walls apply. Running it twice changes nothing. It refuses a production run.

Person /auth/dev-login?email= Acme Review Northwind
Ada Lovelace ada@example.test owner admin
Grace Hopper grace@example.test member owner
Linus Torvalds linus@example.test viewer none

Two organizations, so the organization switcher has something to switch between; Northwind carries a tenant brand (its own name and colours on every page), Acme Review the app's. Each has a general workspace with a demo project, and Acme Review's notes project holds two notes.

The platform, and what refuses to start

The platform's variables (CONUSAI_API_URL, CONUSAI_API_TOKEN, CONUSAI_PROJECT_ID, and CONUSAI_ORG_ID in production) are injected into every service on ConusAI Cloud. A process that does not have them does not start: loadAppEnv (config/src/env.ts) throws a PlatformEnvError that names the variable. The fake platform is used only when you ask for it, with CONUSAI_FAKE_PLATFORM=1, and only in a development or test run (NODE_ENV=development or test; a process with no NODE_ENV is a production one). The app refuses that switch in a production run, and refuses it when CONUSAI_API_URL is not on this machine or the compose network. pnpm dev, compose.dev.yaml and pnpm e2e set it; nothing else does.

Health, readiness and observability

A boot-time deployment-contract refusal is different from a store that becomes unavailable. Unsupported MariaDB/MongoDB composition is checked before the lazy runtime opens a store: ProfileCompositionError preserves every adopted module and its data-rights hooks. Such a process answers /api/ready with 503, ready: false, checks.contract: false and a safe configuration reason; other dynamic routes, including health, answer a plain 503. Responses are no-store with Retry-After: 30. A valid process retains the ordinary liveness/readiness behavior below. Unexpected registry/import/runtime failures are not relabeled as configuration.

  • GET /api/health is liveness: the process is up, and no connection is opened to answer it.
  • GET /api/ready is readiness, and what compose.yaml and conusai.json (health_check) both probe: the runtime role answers, the global reader answers, and every migration this build expects has run. The platform is reported, not required. The answer is booleans and nothing else; the reason goes to the log.
  • Errors, traces and logs are sent through @conusai-sdk/observe-node (web, with @conusai-sdk/observe-start for the browser) when the platform provides CONUSAI_DSN (errors) and OTEL_EXPORTER_OTLP_ENDPOINT (logs and traces). Without them the app runs, sends nothing, and logs one line at start-up that says which is off. Secrets, tokens, connection strings and e-mail addresses are taken out before anything leaves the process (config/src/observability.ts), and every log line and span carries the request's trace id.
  • The browser SDK starts with consent: "anonymous" and no banner. Before serving EU visitors, switch to opt-in with a banner: consent in an app. The rules for cookies, storage, AI disclosure, retention and data-subject rights are in the compliance library.

Sign-in: managed Identity or managed Better Auth

The app signs people in through OIDC (AUTH_PROVIDER=oidc, the default) with @conusai-kit/auth: the server holds the tokens, the browser holds one sealed cookie. On ConusAI Cloud the issuer is a managed identity service linked to the project, and you choose which one by the service you link. App code, conusai.json and the variables are the same for both:

Service type Engine People sign in with Access to this app
identity Zitadel its hosted login (password, passkey, MFA) a role user or admin on the project, given by e-mail address: the console's identity card or POST …/services/{id}/identity/grants
better-auth Better Auth an invitation e-mail's one-use link (sign-up is closed); passkeys after verification an allowed grant on the person's subject: conusai services identity invite, then grant
  1. Create the service in the project's workspace (console Services, or the services API with {"service_type": "better-auth", "parameters": {"backing_postgres_service": <id>}}). Better Auth keeps its accounts in a database of its own on that Postgres service, never in the app's.
  2. Link it to the project (console Linked Services, or conusai services link). A project links one identity service: linking the second engine beside the first is refused. conusai.json declares {"type": "identity"}, which either engine satisfies.
  3. Deploy. Either engine injects OIDC_ISSUER, OIDC_WEB_CLIENT_ID and OIDC_AUDIENCE. identity (Zitadel) adds OIDC_NATIVE_CLIENT_ID. Better Auth adds OIDC_SCOPES, AUTH_PROVIDER_GENERATION, OIDC_RESOURCE (equal to OIDC_AUDIENCE) and the server-only status credential OIDC_RESOURCE_STATUS_URL, OIDC_INTROSPECTION_CLIENT_ID and OIDC_INTROSPECTION_CLIENT_SECRET, with which the app re-checks a person's grant at least every 60 seconds, and OIDC_BACKCHANNEL_URL, the issuer's private address the app server sends its own OIDC requests to (the full table). A link that issued an admission credential also injects OIDC_ADMISSION_TOKEN (server-only) and OIDC_LINK_ID: an invitation the app makes then asks the platform to admit the invited address, which the issuer mails its onboarding link. You set only AUTH_SESSION_SECRET.
  4. Give people access (the table above). The issuer decides who may enter the app and as which of user or admin (principal.appAccess); organizations, workspaces and permissions inside the app are the app's own. With APP_ORGANIZATION_SELF_SERVICE=false only an admin may create an organization; everyone else joins by invitation.

Better Auth delivers its invitation e-mails through the organization's e-mail provider on ConusAI Cloud (console Settings → Notifications); without one an invitation answers 503. Its hosted /login page does not have a sign-in form yet, so a person signs in with the link of their latest invitation e-mail and then opens the app; send a new invitation to sign someone in again (plan).

Changing the engine of an app that already has users is a migration that keeps every app user, not a new link: conusai services identity switch (spec §10). AUTH_PROVIDER=convex-better-auth is something else: Better Auth embedded in the app's own Convex deployment (profiles/convex/README.md).

What is in here

Path What
apps/web the TanStack Start shell: routes and the one HTTP mount (/api/v1/<procedure>)
apps/worker jobs, module cron, the outbox relay, and the migrate job
modules/ your domain modules, one folder each, and the module list in index.ts
config/ the deployment contract per process, the app brand, the feature flags
migrations/postgres/ forward-only SQL, starting with the kernel schema
openapi.json the generated OpenAPI 3.1 document; pnpm openapi rewrites it
conusai.json, compose.yaml, Dockerfile how ConusAI Cloud builds and runs the three processes
compose.dev.yaml, scripts/dev.mjs, .env.example the local stack and every variable the app reads

The three Postgres roles

A linked managed Postgres gives this app three logins, and each process holds only what it needs:

Process Role Variable
migrate owner POSTGRES_MIGRATE_URL (only the service that declares postgres.migrate)
web, worker runtime, bound by row-level security POSTGRES_URL
web, worker global reader POSTGRES_READER_URL

The app refuses to start a web or worker process that was handed the owner's URL, and there is no SET ROLE anywhere: the separation is three separate logins. The reader holds no table privilege except the grants in migrations/postgres (which organizations a user is in, the projects the app serves, an API key by its hash, and the outbox the relay drains), built from what each kit module declares it reads across tenants.

The platform's cluster links now use the same three roles: POSTGRES_URL names the runtime role, and the administrator credential stays in the provisioning path. Cluster URLs list running data nodes with target_session_attrs=read-write; see managed Postgres roles: cluster topology. Verify the app driver's multi-host compatibility and live failover before using a cluster in production; the three-role credential contract alone does not establish those behaviors.

Writing your domain

Add modules; do not edit the shell. docs/domain-start.md says exactly where to add a module, an entity, a migration, a job, a page, a message catalogue, a permission, an audit event and a usage meter, with the commands.

Making it yours

  1. config/src/brand.ts: the product name, the logos and any --brand-* override. Attribution is off by default.
  2. apps/web/public/: your own logo files, which it names. Until then the tab icon is the ConusAI mark from @conusai-kit/brand.
  3. apps/web/src/ui/messages.ts: the app's own messages, in both locales.
  4. Then write your first module: docs/domain-start.md, and the rules in AGENTS.md.

Licence

The scaffold and the @conusai-kit/* packages are licensed under the ConusAI Template Licence (LICENSE-TEMPLATE, a placeholder until counsel decides; the files it covers are listed in TEMPLATE-FILES and carry an SPDX header); the @conusai-sdk/* packages are Apache-2.0. Your modules are yours, and LICENSE is yours to fill in. Third-party software is in THIRD-PARTY-NOTICES (node scripts/licence.mjs --check checks the layout).

Large uploads and the worker's clock

A file of 10 to 100 MB is not a JSON document, so the files module takes it in three calls the page makes through uploadFile of @conusai-kit/ui: files.file.beginUpload (type and size checked first), one files.file.putPart per piece (the piece is the body of a PUT /api/v1/files.file.putPart, held to the size and the type it declared as it streams, with a progress bar on the page), and files.file.completeUpload (the platform's SHA-256 of the stored bytes). The server entry (apps/web/src/server.ts) reads no body, so a piece reaches the handler as it arrives. The default limit is 50 MiB (FILES_POLICY).

apps/worker/src/schedules.ts is the module cron of the app. It turns on files.recover, every five minutes, for every project of every active organization (the worker lists the projects through the global reader); without it a crashed upload would stay a pending row. It also turns on the account sweeps and the hourly agent.runs.expire, which deletes the finished agent runs past AGENT_RUN_RETENTION_DAYS (90 by default). The mariadb and mongodb workers install the same schedules of the modules installed on their profile (apps/worker/src/dialect/schedules.ts).

The example modules

  • modules/documents is a generic document-review module and the pattern for a product's own: upload (the files module) -> an OCR job through @conusai-sdk/ocr (submit, wait) -> a model, through the kit's ModelGateway with structured output, reads each page by the class the reader gave it (a scan or a table as text and page image, drawings and empty pages not at all and reported on the document, DOCUMENTS_EXCLUDED_PAGE_CLASSES) and proposes lines -> code checks that every quote is on the page it cites, and marks the lines it cannot verify needs_review -> the lines are approval requests (the proposals module) with their page, box and verbatim quote and the reader's block id -> a person approves, edits or rejects each one -> the Excel export holds the approved lines and nothing else. OCR results and model replies are cached by content hash, and a model that refuses, runs out of tokens or answers outside the schema ends the document with a typed failure. It has no customer vocabulary: what a line means, and the file format a customer's other systems import, belong to the product's own module (the prompt and schema in adapters/model-extractor.ts and a second exporter are the seams). evals/ scores the extraction on a golden set: see evals/README.md. The model is AI_MODEL (default anthropic/claude-sonnet-5).

  • modules/branding is the organization's own brand: a display name, two colours, two logos, applied by the organization layout on the server and previewed in both themes with the contrast of each colour.

  • modules/notes is the small example that uses every seam of a module: notes with attachments, the attachment uploaded through the files module (the platform's Blob store), an audit event for every change and a usage fact per note, a feature flag (notes.attachments), both languages, pages the web shell mounts from modules/index.ts, and isolation fixtures in its own folder. It is the one to copy.

pnpm e2e runs them in a browser against the whole app (Docker for Postgres, Chromium).

S
Description
ConusAI managed vendor template; policy-reader=1
Readme
1.4 MiB
Languages
TypeScript 95.5%
JavaScript 3.4%
Shell 0.4%
CSS 0.3%
PLpgSQL 0.2%
Other 0.2%