# Cuitty — full doc corpus > Generated 2026-05-14T18:48:50.724Z from 38 doc pages. Each section below begins with a `` marker so agents can split this corpus into individual pages without re-fetching. --- # Quickstart This guide walks you from a clean machine to a running Cuitty portal in under five minutes. By the end you will have: - The Cuitty portal at `http://localhost:7700` - Postgres + SpiceDB + libSQL running in containers - A first project, an API key, and your first event in the audit module ## Prerequisites - Docker 24 or later - Docker Compose v2 - 2 GB of free RAM - Ports `7700`, `5432`, and `50051` available ## 1. Clone the repository ```bash git clone https://gitlab.com/cuitty/root cuitty cd cuitty ``` ## 2. Bring up the stack ```bash docker compose up -d ``` Compose starts four services: | Service | Purpose | | ----------- | -------------------------------------- | | `portal` | The Astro + Bun portal on `:7700` | | `postgres` | BetterAuth + project metadata | | `spicedb` | Fine-grained RBAC | | `libsql` | Per-module event storage | Wait roughly twenty seconds for the health checks to pass. ## 3. Open the portal Visit `http://localhost:7700`. The first request runs the bootstrap flow: you create the root admin account and pick an organization name. There are no seed users. ## 4. Create a project and an API key From the portal, navigate to **Projects → New Project**, then **Settings → API Keys → Create**. Copy the key — you will not see it again. ## 5. Send your first event ```bash curl -X POST http://localhost:7700/api/ingest \ -H "Authorization: Bearer $CUITTY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "audit", "ts": "2026-04-27T12:00:00Z", "data": { "actor": "you@example.com", "action": "quickstart.complete", "resource": "cuitty" } } ] }' ``` You should see `{"accepted":1,"rejected":[]}`. Open the **Audit** module and your event is there. ## Where to go next - [Wire protocol reference](/docs/reference/wire-protocol) — the canonical contract for `/api/ingest` - [Install on Kubernetes](/docs/install/kubernetes) — production deployment - [TypeScript SDK](/docs/sdk/typescript) — `@cuitty/sdk` for Node and Bun - [Modules overview](/docs/modules/audit) — what each module captures --- # Install with Docker Compose Docker Compose is the fastest way to run Cuitty in production-like conditions on a single VM. The reference compose file lives at the root of the `cuitty` repo. ## Reference compose file ```yaml version: "3.9" services: postgres: image: postgres:16 environment: POSTGRES_USER: cuitty POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: cuitty volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U cuitty"] interval: 5s timeout: 3s retries: 12 spicedb: image: authzed/spicedb:latest command: serve --grpc-preshared-key ${SPICEDB_PRESHARED_KEY} ports: - "50051:50051" portal: image: ghcr.io/cuitty/portal:latest depends_on: postgres: { condition: service_healthy } spicedb: { condition: service_started } ports: - "7700:7700" environment: DATABASE_URL: postgres://cuitty:${POSTGRES_PASSWORD}@postgres:5432/cuitty SPICEDB_URL: spicedb:50051 SPICEDB_TOKEN: ${SPICEDB_PRESHARED_KEY} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} volumes: pgdata: ``` Create a `.env` file with `POSTGRES_PASSWORD`, `SPICEDB_PRESHARED_KEY`, and a 32-byte random `BETTER_AUTH_SECRET`. Then: ```bash docker compose up -d ``` ## Backups The portal stores nothing important on its own filesystem — every byte that matters lives in the `postgres` and `libsql` volumes. Snapshot the named volumes nightly with `pg_dump` and a `tar` of the libSQL data directory. ## Upgrades ```bash docker compose pull portal docker compose up -d portal ``` The portal runs migrations on startup. Always snapshot first. ## See also - [Install on Kubernetes](/docs/install/kubernetes) - [Install on bare metal](/docs/install/bare-metal) - [Install on cloud providers](/docs/install/cloud) --- # Install on Kubernetes The official Helm chart deploys Cuitty as a stateful application backed by an external Postgres and an in-cluster SpiceDB. ## Prerequisites - Kubernetes 1.27 or newer - Helm 3.13 or newer - An external Postgres 15+ (RDS, Cloud SQL, or self-managed) - A persistent volume class for libSQL data ## Install the chart ```bash helm repo add cuitty https://charts.cuitty.com helm repo update helm install cuitty cuitty/cuitty \ --namespace cuitty --create-namespace \ --set postgres.url=postgres://cuitty@db.example.com/cuitty \ --set auth.secretRef=cuitty-secrets ``` The chart provisions: - `Deployment` for the portal (3 replicas by default) - `StatefulSet` for SpiceDB - `PersistentVolumeClaim` for libSQL data - `Service` and optional `Ingress` - `NetworkPolicy` restricting egress to allowlisted destinations ## Values | Key | Default | Notes | | --------------------------- | ------------- | ------------------------------------------------------------- | | `replicaCount` | `3` | Portal HTTP replicas. | | `postgres.url` | required | Postgres DSN. Use a `Secret` and `valueFrom` in production. | | `spicedb.replicaCount` | `2` | SpiceDB replicas. Increase for higher RBAC throughput. | | `libsql.storage.size` | `20Gi` | Per-module event storage. | | `ingress.enabled` | `false` | Enable to expose `/` via your ingress controller. | | `image.tag` | chart version | Pin to a specific portal tag in production. | ## Upgrades Helm runs database migrations as a `Job` before swapping the new pod template. A failed migration aborts the upgrade with the previous version still serving. ```bash helm upgrade cuitty cuitty/cuitty --version 0.4.0 ``` --- # Install on bare metal Cuitty ships a single Bun-bundled binary suitable for running directly on a Linux VM without containers. This is the lowest-overhead deployment option. ## Prerequisites - Linux x86_64 or arm64 - Postgres 15+ (local or remote) - A SpiceDB binary on `PATH` - Bun 1.3+ (only required to rebuild from source) ## Download ```bash curl -fsSL https://releases.cuitty.com/portal/latest/portal-linux-x86_64 \ -o /usr/local/bin/cuitty-portal chmod +x /usr/local/bin/cuitty-portal ``` ## Configure Create `/etc/cuitty/portal.env`: ```dotenv DATABASE_URL=postgres://cuitty@localhost/cuitty SPICEDB_URL=localhost:50051 SPICEDB_TOKEN=... BETTER_AUTH_SECRET=... PORT=7700 DATA_DIR=/var/lib/cuitty ``` ## systemd unit `/etc/systemd/system/cuitty-portal.service`: ```ini [Unit] Description=Cuitty Portal After=network.target postgresql.service [Service] Type=simple EnvironmentFile=/etc/cuitty/portal.env ExecStart=/usr/local/bin/cuitty-portal Restart=on-failure User=cuitty Group=cuitty WorkingDirectory=/var/lib/cuitty [Install] WantedBy=multi-user.target ``` ```bash systemctl daemon-reload systemctl enable --now cuitty-portal ``` ## Reverse proxy Run Caddy or nginx in front of the portal for TLS termination. The portal expects to be reached over HTTPS in production for cookie-based auth to work correctly. --- # Install on cloud providers If you would rather not manage Postgres or SpiceDB yourself, the cloud install templates wire Cuitty to managed equivalents on your provider of choice. ## AWS The AWS reference uses: - **App Runner** for the portal container - **RDS Postgres 16** for auth metadata - **EFS** for libSQL data - **VPC peering** to your existing subnets A Terraform module is published at `https://gitlab.com/cuitty/terraform-aws-cuitty`. ```hcl module "cuitty" { source = "gitlab.com/cuitty/terraform-aws-cuitty" vpc_id = aws_vpc.main.id private_subnet_ids = aws_subnet.private[*].id domain = "cuitty.example.com" } ``` ## GCP The GCP reference uses Cloud Run, Cloud SQL, and Filestore. The Terraform module is at `https://gitlab.com/cuitty/terraform-gcp-cuitty`. ## Azure Container Apps + Azure Database for PostgreSQL. Terraform module at `https://gitlab.com/cuitty/terraform-azure-cuitty`. ## Fly.io Fly.io is the simplest managed-but-not-our-cloud option. A `fly.toml` template lives in the `cuitty` repo at `deploy/fly/`. ```bash fly launch --copy-config --from deploy/fly/fly.toml fly postgres create fly postgres attach fly deploy ``` ## Cuitty Cloud If you want zero-ops, [Cuitty Cloud](/cloud) is the official managed offering on top of the same OSS core. --- # TypeScript SDK `@cuitty/sdk` is the canonical client for sending events from a TypeScript or Bun application. It is single-package, tree-shakeable, fail-silent, and batched. ## Install ```bash bun add @cuitty/sdk # or npm install @cuitty/sdk ``` ## Quickstart ```typescript import { createCuittyClient } from "@cuitty/sdk"; import { auditPlugin } from "@cuitty/sdk/plugins/audit"; const cuitty = createCuittyClient({ portalUrl: "https://app.cuitty.com", projectId: process.env.CUITTY_PROJECT_ID!, apiKey: process.env.CUITTY_API_KEY!, }); cuitty.use(auditPlugin()); cuitty.start(); await cuitty.emit({ type: "audit", timestamp: new Date().toISOString(), data: { actor: "alice@example.com", action: "secret.rotate", resource: "stripe.live_key", }, }); ``` ## Configuration ```typescript interface CuittyConfig { portalUrl: string; // Cuitty portal base URL projectId: string; // Project UUID apiKey: string; // cuitty_sk_... flushInterval?: number; // ms between flushes (default 5000) maxBufferSize?: number; // events before forced flush (default 500) timeout?: number; // per-request HTTP timeout (default 10000) debug?: boolean; // log to stderr (default false) enabled?: boolean; // kill switch (default true) } ``` ## Plugins | Plugin | Import | Captures | | ------------ | --------------------------------------- | ------------------------------------- | | `auditPlugin` | `@cuitty/sdk/plugins/audit` | HTTP request/response audit events | | `logsPlugin` | `@cuitty/sdk/plugins/logs` | Pino log records | | `deploysPlugin` | `@cuitty/sdk/plugins/deploys` | CI/CD deploy events | | `repositoryPlugin` | `@cuitty/sdk/plugins/repository` | Git metadata | | `configsPlugin` | `@cuitty/sdk/plugins/configs` | Config file change watcher | | `costsPlugin` | `@cuitty/sdk/plugins/costs` | Cloud cost metrics | ## Hono adapter ```typescript import { Hono } from "hono"; const app = new Hono(); app.use("*", cuitty.honoMiddleware()); ``` ## Lifecycle ```typescript cuitty.start(); // Begin buffering and flushing await cuitty.flush(); // Force flush (call before process exit) await cuitty.shutdown(); // Flush + stop timers ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/typescript](https://benchmarks.cuitty.com/sdks/typescript). The benchmark harness re-runs on every release tag and posts results back to the marketing site. ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) - [Python SDK](/docs/sdk/python) - [Curl examples](/docs/sdk/curl) --- # Python SDK > **Status: Preview.** The Python SDK is on the Phase 3 roadmap. Until it ships you can use [the wire protocol directly](/docs/reference/wire-protocol) — every Python HTTP client supports the small surface area required. ## Planned API ```python from cuitty import Client c = Client( portal_url="https://app.cuitty.com", project_id=PROJECT_ID, api_key=API_KEY, ) c.audit( actor="alice@example.com", action="secret.rotate", resource="stripe.live_key", ) c.flush() ``` ## Wire-protocol equivalent today ```python import hmac, hashlib, json, os, time import urllib.request body = json.dumps({ "events": [{ "type": "audit", "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "data": {"actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key"}, }], }).encode() sig = hmac.new(os.environ["CUITTY_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest() req = urllib.request.Request( "https://app.cuitty.com/api/ingest", data=body, headers={ "Authorization": f"Bearer {os.environ['CUITTY_API_KEY']}", "Content-Type": "application/json", "X-Cuitty-Signature": sig, }, ) print(urllib.request.urlopen(req).read()) ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/python](https://benchmarks.cuitty.com/sdks/python). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # Go SDK > **Status: Preview.** The Go SDK is on the Phase 3 roadmap. Use the [wire protocol](/docs/reference/wire-protocol) until then. ## Planned API ```go c := cuitty.New(cuitty.Config{ PortalURL: "https://app.cuitty.com", ProjectID: id, APIKey: key, }) c.Audit(ctx, cuitty.AuditEvent{ Actor: "alice@example.com", Action: "secret.rotate", Resource: "stripe.live_key", }) ``` ## Wire-protocol equivalent today ```go package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "net/http" "os" ) func main() { body := []byte(`{"events":[{"type":"audit","ts":"2026-04-27T12:00:00Z","data":{"actor":"alice@example.com","action":"secret.rotate","resource":"stripe.live_key"}}]}`) mac := hmac.New(sha256.New, []byte(os.Getenv("CUITTY_WEBHOOK_SECRET"))) mac.Write(body) sig := hex.EncodeToString(mac.Sum(nil)) req, _ := http.NewRequest("POST", "https://app.cuitty.com/api/ingest", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("CUITTY_API_KEY")) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Cuitty-Signature", sig) http.DefaultClient.Do(req) } ``` ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/go](https://benchmarks.cuitty.com/sdks/go). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # Rust SDK > **Status: Preview.** The Rust SDK is on the Phase 3 roadmap. ## Planned API ```rust let c = cuitty::Client::new(cuitty::Config { portal_url, project_id, api_key, }); c.audit(cuitty::AuditEvent { actor: "alice@example.com".into(), action: "secret.rotate".into(), resource: "stripe.live_key".into(), }).await?; ``` Use the [wire protocol](/docs/reference/wire-protocol) and `reqwest` + `hmac` until the typed client lands. ## Live performance numbers See live benchmarks at [https://benchmarks.cuitty.com/sdks/rust](https://benchmarks.cuitty.com/sdks/rust). ## See also - [Wire protocol](/docs/reference/wire-protocol) - [SDK parity & divergence](/docs/sdk/parity) --- # SDK Parity & Divergence The Cuitty SDKs share a wire protocol but each takes the idiomatic shape of its language. This page documents the intentional differences. ## Capability matrix | Feature | TypeScript | Python | Go | Rust | | --- | --- | --- | --- | --- | | Async-first | Implicit (Promise) | Sync (async on roadmap) | Sync, ctx-aware | Async (Tokio) | | Plugin model | `client.use(plugin)` | `client.audit`, `client.logs` | `client.Audit`, `client.Logs` | `client.audit()`, `client.logs()` | | Path B retry | Yes (5xx + 429) | Yes (5xx + 429) | Yes (5xx + 429) | Yes (5xx + 429) | | Idempotency keys | Yes (UUID v4) | Yes | Yes | Yes | | Hono adapter | Yes | n/a | n/a | n/a | | Pino bridge | Yes | n/a (stdlib logging adapter on roadmap) | n/a (slog) | n/a (tracing) | | Concurrency-safe | Yes | Yes | Yes | Yes (Arc-cloned client) | ## Things that ARE the same across all four SDKs - Wire format: identical JSON bodies for the same logical event - Idempotency: UUID v4 in `X-Idempotency-Key` header on every Path B batch - Retry: 3 attempts, exponential backoff (250 ms → 1 s → 4 s) - 4xx behavior: drop with warn log, no retry - 429 behavior: honor `Retry-After` - Auth: `Authorization: Bearer ` plus `X-Project-Id` - Batch triggers: ≥1000 events or ≥1 MiB or ≥250 ms idle - Close semantics: `close()` flushes pending events synchronously If your code depends on something that's the same across SDKs but isn't on this list, it might still be safe — but it's not promised. Open an issue and we'll add it (or document why it's not promised). ## Where to find authoritative behavior The wire protocol is documented in [`packages/wire-protocol/openapi.yaml`](https://gitlab.com/cuitty/wire-protocol/openapi.yaml). The SDK-specific READMEs document divergences from that protocol in their "Spec Drift" sections. Live SDK-by-SDK benchmarks: [https://benchmarks.cuitty.com/sdks](https://benchmarks.cuitty.com/sdks). --- # cURL examples For one-off scripts, CI hooks, and shell-only environments, the wire protocol is reachable from a plain `curl`. ## Send an audit event ```bash BODY='{ "events": [{ "type": "audit", "ts": "2026-04-27T12:34:56Z", "data": { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key" } }] }' SIG=$(printf '%s' "$BODY" \ | openssl dgst -sha256 -hmac "$CUITTY_WEBHOOK_SECRET" -hex \ | awk '{print $2}') curl -X POST https://app.cuitty.com/api/ingest \ -H "Authorization: Bearer $CUITTY_API_KEY" \ -H "X-Cuitty-Signature: $SIG" \ -H "Content-Type: application/json" \ -d "$BODY" ``` ## Health check ```bash curl https://app.cuitty.com/api/health ``` ## Idempotent retries Add an `X-Cuitty-Idempotency-Key` header (any UUID). The portal deduplicates retries with the same key for 24 hours. ```bash curl -X POST https://app.cuitty.com/api/ingest \ -H "X-Cuitty-Idempotency-Key: $(uuidgen)" \ ... ``` --- # Protocol overview The SDK is a convenience layer. Everything underneath is a single HTTP endpoint accepting a batch of typed events. ``` ┌───────────────────┐ Application ───► │ @cuitty/sdk │ ─── HMAC-signed POST ───► Portal │ (or curl, etc.) │ └───────────────────┘ ``` ## Three things the SDK gives you 1. **Batching.** Events accumulate in memory and flush every 5 seconds, or on buffer fill, or on `shutdown()`. 2. **Signing.** HMAC-SHA256 over the body using your API key — no need to hand-roll OpenSSL. 3. **Plugins.** Tap into framework middleware, Pino, or your CI pipeline without rewriting your app. ## Three things the SDK does *not* do - Poll Cuitty for data — the SDK is push-only. - Retry indefinitely — failed batches drop after `maxRetries` and are logged locally. - Throw exceptions on transport errors — fail-silent is a deliberate design choice; SDK errors must never crash the host process. ## The contract The contract between SDK and portal is documented in full at [Wire protocol](/docs/reference/wire-protocol). Anything that conforms to the contract works — implement your own client in any language. --- # Wire protocol The wire protocol is the **single source of truth** for how data enters Cuitty. Every SDK, plugin, and integration is just a typed wrapper over this one endpoint. If you can sign a JSON blob and POST it over HTTPS, you can integrate Cuitty. ## Endpoint ``` POST {portalUrl}/api/ingest ``` `portalUrl` is the base URL of your portal — `http://localhost:7700` for a local install, `https://app.cuitty.com` for Cuitty Cloud, or whatever you configured for self-hosted production. ## Headers | Header | Required | Notes | | ---------------------------- | -------- | ------------------------------------------------------------------ | | `Authorization` | yes | `Bearer `. The key identifies the project. | | `Content-Type` | yes | `application/json`. | | `X-Cuitty-Signature` | yes | `hex(hmac_sha256(webhook_secret, body))` — verifies authenticity. | | `X-Cuitty-Idempotency-Key` | no | UUID. Repeated POSTs with the same key are deduplicated for 24 h. | ## Body ```json { "events": [ { "type": "audit", "ts": "2026-04-27T12:34:56Z", "data": { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key" } } ] } ``` `events` is a JSON array. Each entry has: - `type` — one of `audit`, `config`, `cost`, `deploy`, `error`, `feature_flag`, `log`, `performance`, `repository`, `tenant`, `trace`, `video`, or `webhook`. Determines which module receives the event. - `ts` — ISO 8601 timestamp in UTC. - `data` — module-specific payload. See the [event reference](#event-types) below. ## Response ```json { "accepted": 1, "rejected": [] } ``` If any event in the batch is malformed, the portal accepts the valid ones and reports the rest: ```json { "accepted": 2, "rejected": [ { "index": 1, "reason": "data.actor: required" } ] } ``` ## Status codes | Code | Meaning | | ---- | --------------------------------------------------------------------- | | 200 | Batch processed — see body for per-event accept/reject breakdown. | | 400 | Body is not valid JSON, or `events` is missing. | | 401 | Missing or invalid `Authorization` bearer. | | 403 | Signature does not match. | | 413 | Batch exceeds the 5 MB size limit. Split into smaller batches. | | 429 | Rate limit hit. Retry with exponential backoff. | | 503 | The portal is shedding load. The SDK retries; raw clients should too. | ## Event types ### audit ```json { "actor": "alice@example.com", "action": "secret.rotate", "resource": "stripe.live_key", "method": "POST", "path": "/api/secrets/rotate", "statusCode": 200, "duration": 32, "ip": "203.0.113.5", "userAgent": "Mozilla/5.0 ...", "metadata": {} } ``` ### log ```json { "level": "info", "message": "request processed", "module": "api", "fields": { "requestId": "abc" } } ``` ### deploy ```json { "service": "api", "version": "v1.4.2", "environment": "production", "status": "succeeded", "commit": "ab12cd34" } ``` ### repository ```json { "provider": "github", "repo": "acme/api", "branch": "main", "commit": "ab12cd34", "author": "alice" } ``` ### config ```json { "path": "config/production.toml", "diff": "--- old\n+++ new\n@@ -1 +1 @@\n-foo=1\n+foo=2", "actor": "alice" } ``` ### cost ```json { "provider": "gcp", "service": "compute", "region": "us-central1", "amount": 1234.56, "currency": "USD", "period": "2026-04" } ``` ### performance ```json { "service": "api", "metric": "p99_latency", "value": 142.3, "unit": "ms" } ``` ### trace ```json { "traceId": "trace_01", "spanId": "span_01", "serviceName": "portal", "name": "GET /api/projects", "durationMs": 42 } ``` ### error ```json { "name": "TypeError", "message": "Cannot read properties of undefined", "release": "portal@0.3.0", "environment": "production" } ``` ### feature_flag ```json { "key": "checkout_redesign", "enabled": true, "rollout": 25, "actor": "alice@example.com" } ``` ### webhook ```json { "destination": "deploy-alerts", "url": "https://example.com/hooks/cuitty", "statusCode": 200, "attempt": 1 } ``` ### tenant ```json { "projectId": "proj_01", "plan": "team", "ingestLimitPerSecond": 1000 } ``` ### video ```json { "jobId": "video_01", "targetUrl": "https://app.cuitty.com", "status": "completed", "artifactUrl": "https://cdn.cuitty.com/videos/video_01.mp4" } ``` ## Signing ``` signature = hex(hmac_sha256(webhook_secret, body)) ``` The `webhook_secret` is generated alongside the API key in the portal. Send the hex digest with no `sha256=` prefix — the portal expects the bare hex string. A reference implementation in TypeScript: ```typescript import { createHmac } from "node:crypto"; function sign(body: string, secret: string): string { return createHmac("sha256", secret).update(body).digest("hex"); } ``` ## Idempotency Pass a UUID in `X-Cuitty-Idempotency-Key` to make retries safe. The portal stores the key for 24 hours and short-circuits duplicate POSTs to a 200 with the original response body. ## Rate limits The default per-project rate limit is **1,000 events per second**, **100 batches per second**, **5 MB per batch**. All three are configurable in self-hosted installs via `INGEST_RATE_LIMIT_*` environment variables. --- # API reference The Cuitty portal exposes a small REST surface beyond `/api/ingest` for project metadata, health, and module-specific reads. Every endpoint requires a bearer token unless explicitly noted. ## Health ``` GET /api/health ``` Returns `200 { "status": "ok" }` when the portal can reach Postgres and SpiceDB. Used by load balancers and the SDK during connection probes. Public — no auth required. ## Projects ``` GET /api/projects List projects in the org POST /api/projects Create a project GET /api/projects/{id} Read a project PATCH /api/projects/{id} Update a project DELETE /api/projects/{id} Soft-delete a project ``` ## API keys ``` GET /api/projects/{id}/keys List API keys POST /api/projects/{id}/keys Create an API key (returns key once) DELETE /api/projects/{id}/keys/{kid} Revoke a key ``` ## Module-specific reads Each module exposes its own read endpoints under `/api/modules/{module}/`. See the [module documentation](/docs/modules/audit) for the full surface. | Module | Base path | | ------------ | ------------------------------- | | Audit | `/api/modules/audit/` | | Costs | `/api/modules/costs/` | | Deploys | `/api/modules/deploys/` | | Logs | `/api/modules/logs/` | | Configs | `/api/modules/configs/` | | Repository | `/api/modules/repository/` | | Performance | `/api/modules/performance/` | | Traces | `/api/traces/` | | Errors | `/api/errors/` | | Feature Flags | `/api/flags/` | | Webhooks | `/api/webhooks/` | | Tenants | `/api/tenants/` | | Video | `/api/video/` | ## OpenAPI A live OpenAPI 3.1 document is published at `/api/openapi.json` on every running portal. --- # CLI reference `cui` is the official command-line companion. It wraps the REST API for everyday operator tasks. ## Install ```bash curl -fsSL https://releases.cuitty.com/cli/install.sh | sh ``` ## Auth ```bash cui login --portal https://app.cuitty.com cui logout cui whoami ``` ## Projects ```bash cui project list cui project create cui project use cui project keys create ``` ## Modules ```bash cui audit list --project --since 24h cui logs tail --project --service api cui deploys list --env production cui costs report --period 2026-04 ``` ## Send a one-off event ```bash cui send audit \ --actor alice@example.com \ --action secret.rotate \ --resource stripe.live_key ``` ## Self-hosted operator commands ```bash cui admin migrate # Run pending migrations cui admin backup # Snapshot Postgres + libSQL cui admin verify-chain # Verify the audit-chain integrity ``` --- # Audit module The audit module records every administrative action with a hash-chained log entry — a single mutation cannot be silently rewritten without breaking the chain. ## What it captures - Actor (user ID + email) - Action verb (e.g. `project.create`, `secret.rotate`) - Resource type and id - Before/after diff of the resource - HTTP method, path, status code, duration - IP and User-Agent - Optional metadata (auth method, scopes) ## How to send events The TypeScript audit plugin instruments your framework middleware automatically. From any other language, POST a `type: "audit"` event to `/api/ingest`. See [Wire protocol](/docs/reference/wire-protocol#audit) for the schema. ## Hash chain Every audit entry stores `prev_hash = hash(prev_entry || row_payload)`. The portal exposes `cui admin verify-chain` to re-walk and verify the chain. Tampering with a row breaks every subsequent hash. ## Reads | Endpoint | Returns | | ---------------------------------------- | -------------------------------------- | | `GET /api/modules/audit/events` | Paginated list of recent audit events | | `GET /api/modules/audit/events/{id}` | A single event with full payload | | `GET /api/modules/audit/verify` | Chain-integrity report | --- # Costs module The costs module aggregates daily spend from cloud billing exports and per-service tags. It is provider-agnostic — you push the rolled-up `cost` events to `/api/ingest` and Cuitty handles storage, attribution, and alerting. ## Supported providers - GCP (Cloud Billing export to BigQuery) - AWS (Cost and Usage Report) - Azure (Cost Management exports) - DigitalOcean (Billing API) ## Event shape ```json { "type": "cost", "ts": "2026-04-27T00:00:00Z", "data": { "provider": "gcp", "service": "compute", "region": "us-central1", "amount": 1234.56, "currency": "USD", "period": "2026-04" } } ``` ## Budgets Define budgets per project, service, or tag. The portal emits a webhook and an in-app alert when a budget crosses 50%, 80%, and 100%. --- # Deploys module The deploys module records every deployment event — when, where, what version, and whether it succeeded. It pairs naturally with CI pipelines and feature-flag systems. ## Event shape ```json { "type": "deploy", "ts": "2026-04-27T13:00:00Z", "data": { "service": "api", "version": "v1.4.2", "environment": "production", "status": "succeeded", "commit": "ab12cd34", "duration": 87 } } ``` ## Integrations Built-in plugins exist for GitHub Actions, GitLab CI, CircleCI, and Buildkite. Each emits the deploy event automatically when your pipeline reaches the `deploy` step. --- # Logs module The logs module is a low-noise structured log store. It indexes by `level`, `module`, and `service`, but does not attempt to compete with full-text logging stores. Cuitty assumes you keep heavy log volumes elsewhere — ship the high-signal subset here. ## Event shape ```json { "type": "log", "ts": "2026-04-27T13:00:00Z", "data": { "level": "info", "message": "request processed", "module": "api", "service": "api", "fields": { "requestId": "abc", "userId": "user_123" } } } ``` ## Pino bridge The TypeScript SDK ships a Pino multistream adapter so adding Cuitty as a destination is one line: ```typescript cuitty.use(logsPlugin({ logger: pino, minLevel: "info" })); ``` --- # Configs module The configs module records every configuration file change — who, what, when, and the unified diff. It is intentionally **not** a config server; it observes the changes and records them for audit and rollback. --- # Repository module The repository module ingests Git metadata events — commits, branches, tags — to give an at-a-glance view of activity across your repos. --- # Performance module The performance module captures lightweight process-level metrics: CPU, memory, event-loop lag, GC pauses. It is **not** an APM — it complements Datadog/Honeycomb rather than replacing them. --- # Traces module The traces module accepts OTLP-style JSON spans and stores them in PostgreSQL tables keyed by project, trace id, and timestamp for request waterfall and service-map views. ## What it captures - Trace id, span id, and parent span id - Service name, operation name, and span kind - Start and end timestamps - Status code and status message - Attributes, resource attributes, and events ## Ingest ``` POST /api/traces/ingest ``` The JSON path is the first supported transport. OTLP protobuf support is still planned. ## Storage Self-hosted installs use stock PostgreSQL. High-volume span tables are partitioned by timestamp, and the portal reads rollup tables for summary views so trace detail remains available without requiring a PostgreSQL extension. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/traces/status` | Module health | | `GET /api/traces?project_id=...` | Recent traces | | `GET /api/traces/{traceId}` | A trace waterfall | Product page: [Traces](/product/modules/traces). --- # Errors module The errors module captures exceptions, computes a stable fingerprint, groups related occurrences, and exposes grouped reads for triage. ## What it captures - Exception name, message, and stack trace - Release, environment, service, and request context - Fingerprint and group status - First seen, last seen, and occurrence counts ## Capture ``` POST /api/errors/capture ``` Unknown fields are kept in metadata so SDKs can ship richer context without a schema migration for every framework. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/errors/status` | Module health | | `GET /api/errors/groups?project_id=...` | Grouped exceptions | | `GET /api/errors/groups/{fingerprint}` | Group detail and recent occurrences | Product page: [Errors](/product/modules/errors). --- # Feature Flags module The feature-flags module manages flag definitions, rollout percentages, cohort gates, kill switches, and transactional history. ## Evaluation SDK helpers evaluate locally using the same rule as the server: 1. Killed flags return false. 2. Disabled flags return false. 3. Non-empty cohort dimensions must match the evaluation context. 4. Percentage rollouts use SHA-256 of `flagKey:userId`, bucketed into 0-99. ## API | Endpoint | Returns | | --- | --- | | `GET /api/flags/status` | Module health | | `GET /api/flags?project_id=...` | Flags for a project | | `GET /api/flags/{key}?project_id=...` | A single flag | | `GET /api/flags/stream?project_id=...` | Server-sent update stream | Product page: [Feature Flags](/product/modules/feature-flags). --- # Webhooks module The webhooks module gives Cuitty modules one shared delivery lane for customer-owned HTTP endpoints, Slack-style integrations, and internal notifications. ## Delivery model - Endpoints own their signing secret. - Deliveries record request and response metadata. - Retries are explicit attempts with their own status. - Secret rotation keeps old attempts inspectable. ## API | Endpoint | Returns | | --- | --- | | `GET /api/webhooks/status` | Module health | | `GET /api/webhooks/endpoints?project_id=...` | Endpoint list | | `POST /api/webhooks/deliver` | Internal delivery trigger | | `POST /api/webhooks/endpoints/{id}/rotate` | Rotate endpoint secret | Product page: [Webhooks](/product/modules/webhooks). --- # Tenants module The tenants module stores Cuitty-specific project configuration for multi-tenant installs, including ingest limits, retention defaults, and enabled module policy. ## What it manages - Project configuration - Ingest rate limits - Retention defaults - Enabled module lists - Admin-facing tenant metadata ## API | Endpoint | Returns | | --- | --- | | `GET /api/tenants/status` | Module health | | `GET /api/tenants/projects` | Project administration list | | `GET /api/tenants/projects/{projectId}/config` | Project config | | `PATCH /api/tenants/projects/{projectId}/config` | Update project config | Product page: [Tenants](/product/modules/tenants). --- # Plaintext Secrets module The plaintext secrets module scans source code across all projects in your workspace to detect hardcoded secrets — API keys, database credentials, tokens, and private keys that should never be committed. ## How it works Point the scanner at one or more root directories (e.g. `~/Code`). It walks every file that isn't generated (skipping `node_modules`, `target/`, `.venv`, `dist/`, etc.), matches against 53 secret detection patterns, and optionally runs Shannon entropy analysis to catch novel credential formats. Results are persisted in a dedicated SQLite database. Each finding tracks the file, line, column, pattern that matched, severity, and temporal metadata — when the secret was first detected, when it was last seen, and whether it's still present. ## Supported languages and bundlers | Ecosystem | Scanned files | | ---------- | ----------------------------------------------- | | JavaScript | `.ts`, `.js`, `.tsx`, `.jsx`, `.env*`, `.npmrc` | | Rust | `.rs`, `.toml`, `Cargo.toml` | | Python | `.py`, `.cfg`, `.ini`, `.env*` | | Go | `.go`, `.yaml`, `.yml`, `.env*` | | Config | `.json`, `.yaml`, `.yml`, `.toml`, `.properties` | Bundler-specific patterns detect tokens in `.npmrc`, `bunfig.toml`, `Cargo` registry config, PyPI tokens, and Docker registry credentials. ## Pattern categories - **Cloud** — AWS access/secret keys, GCP service accounts and API keys, Azure connection strings, Cloudflare, Fly.io, DigitalOcean - **SCM** — GitHub PATs (classic and fine-grained), GitLab PATs, Bitbucket app passwords - **Payments** — Stripe secret/publishable/webhook keys, PayPal client secrets - **Database** — Connection strings with credentials (Postgres, MongoDB, Redis, MySQL), Turso, Supabase, Firebase - **Messaging** — Slack bot/user/app tokens, Discord tokens, Twilio, SendGrid - **Crypto** — PEM private keys (RSA, EC, PKCS8), JWTs, PGP/SSH private keys - **Generic** — Password assignments, API key assignments, bearer tokens, basic auth headers - **Bundler** — npm auth tokens, Cargo registry tokens, PyPI API tokens, Docker registry passwords ## Extraction modes The standalone JSON index can be configured with three extraction modes: | Mode | Stores | Use case | | ---------------- | ------------------------- | ---------------------------------- | | `key-only` | Key name only | Auditing without exposure risk | | `key-obfuscated` | Key name + masked value | Triage with partial visibility | | `key-raw` | Key name + full value | Remediation workflows | ## Cross-reference engine When the same secret value appears in multiple files or projects, the scanner groups them by SHA-256 hash. The dashboard highlights shared secrets so you can see which projects share credentials and prioritize rotation. ## Entropy detection Beyond pattern matching, the scanner uses Shannon entropy analysis to flag high-entropy strings that may be novel credential formats. Configurable threshold (default: 4.5 bits), minimum length (20 chars), and automatic exclusion of UUIDs, URLs, commit hashes, and long base64 payloads. ## API endpoints | Endpoint | Method | Returns | | ---------------------------------------------------------- | ------ | ------------------------------------ | | `/api/modules/plaintext-secrets/findings` | GET | Paginated findings with filters | | `/api/modules/plaintext-secrets/findings/{id}` | GET | Single finding with cross-references | | `/api/modules/plaintext-secrets/findings/{id}` | POST | Suppress a finding | | `/api/modules/plaintext-secrets/scans` | GET | Scan history | | `/api/modules/plaintext-secrets/scan` | POST | Trigger a new scan | ## Integration events The module emits events via the shared `ModuleEventBus`: - `secret:detected` — fired for each new finding during a scan - `cuitty.plaintext-secrets.scan-completed` — fired when a scan finishes, with summary stats --- # Video module The video module records browser workflows as reviewable artifacts. A job starts from a prompt, target URL, or explicit steps, then a browser runner captures screenshots and writes a video artifact when the run finishes. ## What it captures - Prompt and target URL - Resolved workflow steps - Run status and per-step progress - Duration, output format, and artifact URL - Error details when a run fails ## Submit a job ``` POST /api/video/jobs ``` Use the endpoint for long-running video generation requests. Ingest events with `type: "video"` are reserved for completed-run metadata that should appear in timelines and audit-style views. ## Reads | Endpoint | Returns | | --- | --- | | `GET /api/video/status` | Module health | | `GET /api/video/jobs?project_id=...` | Recent jobs | | `GET /api/video/jobs/{id}` | Job status, steps, and artifact metadata | Product page: [Video Generator](/product/modules/video). --- # Auth app The auth app owns sign-in, session cookies, API-key issuance, and role checks used by the portal and admin surfaces. It is infrastructure for the rest of Cuitty rather than a standalone observability module. ## Responsibilities - Bootstrap the first administrator for a new install - Issue and revoke project API keys - Maintain session state for browser users - Expose authorization context to app and module routes ## Operational notes Production installs should run the portal over HTTPS so browser cookies can be marked secure. API keys are shown once when created; store them in your secret manager and rotate them from the portal when needed. --- # Portal app The portal app is the main browser UI for Cuitty. It hosts project setup, API-key management, module views, and operational review workflows. ## Responsibilities - Project and tenant selection - Module read views and status pages - API-key creation and revocation - Links to audit, deploy, cost, log, trace, and error records ## Runtime notes Local installs serve the portal at `http://localhost:7700` by default. Self-hosted production installs should put the portal behind TLS and configure the public base URL used by SDKs and webhook destinations. --- # Admin app The admin app covers operator-only tasks that should not be mixed into day-to-day project views. It pairs with CLI commands for maintenance and verification. ## Responsibilities - Migration status and install health checks - Audit-chain verification summaries - Tenant and project administration - Backup and restore guidance for self-hosted installs ## Access model Admin actions require an authenticated operator with the required role. Changes that affect projects, keys, tenants, or module configuration should also produce audit records. --- # Forge app The forge app supports project-idea workflows: collect keywords, rank candidate names, and create a repository or workspace when the operator chooses one. ## Responsibilities - Store brainstorming sessions and selected candidates - Create or attach repositories for accepted ideas - Hand created workspaces to the repository module for indexing - Keep session state so interrupted work can resume ## Boundaries Forge records workflow metadata. Repository analysis, commit metadata, and workspace indexing belong to the repository module. --- # Site app The site app serves the public Cuitty website and documentation. It is the place for install guides, SDK notes, module docs, reference pages, changelog entries, and comparison pages. ## Responsibilities - Render docs from the content collection - Publish machine-readable docs as Markdown and JSON - Keep product copy aligned with implemented modules - Link readers to install, SDK, and API reference material ## Content rules Docs should describe implemented behavior plainly. When a feature is planned, mark it as planned or omit it until the implementation exists. --- # Persist Quickstart ## Install ```bash # npm npm install @cuitty/persist # bun bun add @cuitty/persist # pnpm pnpm add @cuitty/persist ``` ## Create a store ```ts import { createStore } from "@cuitty/persist"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", }); ``` ## Basic CRUD ```ts // Put a record await store.records.put("users/alice", { name: "Alice", role: "admin", }); // Get a record const alice = await store.records.get("users/alice"); // List records by prefix const users = await store.records.list("users/"); // Delete a record await store.records.delete("users/alice"); ``` ## Enable sync Add a `sync` block to push local writes to a remote backend automatically. Writes never block -- sync runs in the background. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` See [Sync & Conflict Resolution](/docs/persist/sync) for strategies and conflict handling. ## Enable encryption Set `encrypt: true` to encrypt all data before it leaves the device. The sync server never sees plaintext. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` See [End-to-End Encryption](/docs/persist/encryption) for key management and zero-knowledge architecture. --- # Storage Adapters Persist uses pluggable adapters for storage backends. Every adapter exposes the same API (`store.records`, `store.kv`, `store.blobs`, `store.events`), so you can swap backends without changing application code. ## SQLite The default adapter. Ideal for local-first apps, development, and mobile. ```ts import { createStore } from "@cuitty/persist"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", }); ``` SQLite stores are single-file, need no external process, and support WAL mode for concurrent reads out of the box. ## Postgres Production-grade durability with full SQL support. Best for server-side workloads. ```ts const store = await createStore({ name: "my-app", adapter: "postgres", connection: { host: "localhost", port: 5432, database: "persist", user: "persist", password: process.env.PG_PASSWORD, }, }); ``` ### Connection pooling Pass `pool: { max: 20 }` to control the connection pool size. The default is 10. ## S3 Blob storage adapter for large files and binary data. ```ts const store = await createStore({ name: "my-app", adapter: "s3", bucket: "my-persist-bucket", region: "us-east-1", credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }); ``` The S3 adapter maps `store.blobs` operations to S3 objects and `store.records` to JSON objects stored under a configurable prefix. ## P2P Peer-to-peer replication with no central server. Peers discover each other and sync directly. ```ts const store = await createStore({ name: "my-app", adapter: "p2p", discovery: { method: "mdns", // or "dht" for wide-area topic: "my-app-sync", }, }); ``` The P2P adapter uses CRDTs internally to merge writes from multiple peers without conflicts. ## Custom adapters Implement the `PersistAdapter` interface to build your own backend. ```ts import type { PersistAdapter } from "@cuitty/persist"; const myAdapter: PersistAdapter = { async get(key: string) { /* ... */ }, async put(key: string, value: unknown) { /* ... */ }, async delete(key: string) { /* ... */ }, async list(prefix: string) { /* ... */ }, }; const store = await createStore({ name: "my-app", adapter: myAdapter, }); ``` All four methods must return promises. `list` returns an async iterable of `{ key, value }` pairs. --- # Sync & Conflict Resolution ## How sync works Persist is offline-first. Writes go to the local store immediately and never block on network. A background sync queue pushes changes to the remote and pulls remote changes back. ``` local write --> local store --> sync queue --> remote ^ | pull remote changes ``` ## Configuration ```ts import { createStore } from "@cuitty/persist"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", sync: { remote: "postgres", strategy: "last-write-wins", interval: 5000, // sync every 5 seconds (default: 10000) }, }); ``` ### Options | Option | Type | Default | Description | | ------------ | -------- | ------------------ | ----------------------------------- | | `remote` | `string` | -- | Remote adapter type or connection | | `strategy` | `string` | `"last-write-wins"`| Conflict resolution strategy | | `interval` | `number` | `10000` | Sync interval in milliseconds | ## Strategies ### Last-write-wins (LWW) The simplest strategy. Each record carries a timestamp; the most recent write wins on conflict. ```ts sync: { remote: "postgres", strategy: "last-write-wins", } ``` Good for settings, preferences, and data where overwrites are acceptable. ### CRDTs Conflict-free replicated data types that merge automatically without data loss. Best for collaborative data. ```ts sync: { remote: "postgres", strategy: "crdt", } ``` Persist uses operation-based CRDTs under the hood. Counters, sets, and maps merge deterministically across peers. ### Custom merge Supply a function that receives both versions and returns the resolved value. ```ts sync: { remote: "postgres", strategy: "custom", merge(local, remote) { // Example: keep the record with more fields const localKeys = Object.keys(local.value); const remoteKeys = Object.keys(remote.value); return localKeys.length >= remoteKeys.length ? local : remote; }, } ``` ## Conflict resolution callbacks Register a callback to handle conflicts interactively or log them. ```ts store.events.on("conflict", (event) => { console.log(`Conflict on key: ${event.key}`); console.log("Local:", event.local); console.log("Remote:", event.remote); console.log("Resolved:", event.resolved); }); ``` ## Offline-first behavior When the device is offline, Persist continues to accept reads and writes against the local store. Changes accumulate in the sync queue and flush automatically when connectivity returns. The sync queue is persisted to disk, so pending changes survive app restarts. ## Monitoring sync status ```ts const status = store.sync.status(); // { state: "synced" | "syncing" | "offline", pending: number, lastSync: Date } store.events.on("sync", (event) => { console.log(`Synced ${event.pushed} up, ${event.pulled} down`); }); ``` --- # End-to-End Encryption ## Enable encryption ```ts import { createStore } from "@cuitty/persist"; const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, }); ``` When `encrypt: true` is set, all data is encrypted on the device before it is written to disk or sent over the network. The sync server, storage backend, and any intermediary never see plaintext. ## How it works Persist generates a 256-bit device key on first run and stores it in the OS keychain (or a local keyfile as fallback). Every record is encrypted with AES-256-GCM using a per-record nonce derived from the key and the record's path. ``` plaintext --> AES-256-GCM encrypt --> ciphertext --> store / sync ``` Reads reverse the process transparently. Application code works with plain objects -- encryption and decryption are invisible to the caller. ## Key management ### Device keys Each device generates its own key on first use. The key never leaves the device unless explicitly exported. ```ts // Export the device key (for backup or migration) const exportedKey = await store.crypto.exportKey(); // Import a key on a new device const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, key: exportedKey, }); ``` ### Key rotation Rotate the encryption key without downtime. Persist re-encrypts existing records in the background. ```ts await store.crypto.rotateKey(); ``` After rotation the old key is kept in a sealed keyring so previously-synced peers can still decrypt historical data until they receive the new key. ## Encrypted sync When encryption and sync are both enabled, peers exchange ciphertext. Decryption happens only on the receiving device using a shared key. ```ts const store = await createStore({ name: "my-app", adapter: "sqlite", path: "./data/my-app.db", encrypt: true, sync: { remote: "postgres", strategy: "last-write-wins", }, }); ``` ### Sharing keys between peers Peers use a Diffie-Hellman key exchange to establish a shared secret. No key material is transmitted in the clear. ```ts // On device A: generate an invite const invite = await store.crypto.createInvite(); // --> send `invite.code` to device B out-of-band // On device B: accept the invite await store.crypto.acceptInvite(invite.code); ``` Once paired, both devices derive the same data key and can decrypt each other's records. ## Zero-knowledge architecture The sync server stores only ciphertext and opaque metadata (record size, sync timestamps). It cannot: - Read record contents - Identify record types or schemas - Correlate records across stores (keys are encrypted too) Even if the server is compromised, an attacker gains no usable data without the device keys. ---