194 lines
12 KiB
Markdown
194 lines
12 KiB
Markdown
<!-- SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 -->
|
|
# 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](AGENTS.md) first. Claude Code reads it through
|
|
[CLAUDE.md](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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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](../../docs/how-to/consent-in-an-app.md). The rules for cookies,
|
|
storage, AI disclosure, retention and data-subject rights are in the
|
|
[compliance library](../../docs/compliance/README.md).
|
|
|
|
## 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](../../docs/architecture/managed-postgres-roles.md#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](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](docs/domain-start.md), and the rules in [AGENTS.md](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](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).
|