Files

12 KiB

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 (next 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.

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 /_next/static; do not set Next.js assetPrefix from a deployment-time value. 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

  • 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-next 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.

What is in here

Path What
apps/web the Next.js 16 shell: routes, layouts 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/favicon.svg and the two logo files it names.
  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). apps/web/proxy.ts does not run for application/octet-stream bodies: Next.js would otherwise cut them at 10 MB. 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.

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).