# IIOS — Claude Code Rules

> **Project memory:** granular, versioned per-topic notes live in **`.claude/memory/`** — read
> `.claude/memory/MEMORY.md` (the index) and the relevant entries before working in an area, and
> add/update memories as the project evolves. This file is the canonical rules; those are the
> accumulating notes.

## What IIOS is

**IIOS (Insignia Interaction OS)** is a **generic, multi-tenant "interaction OS"** — one
NestJS service (`@insignia/iios-service`) + a family of SDKs. Every interaction (a chat
message, a support ticket, a routed post, an AI suggestion, a meeting) is the **same kernel
object** inside a **tenant scope**, behind the **same fail-closed gates** (policy/consent),
emitting the **same audit trail**. Products (messaging, support, community, AI, meetings) are
thin **specializations** on top of the kernel — never the other way around.

`chat-web` (separate repo) is the reference consumer app.

## THE #1 LOCKED RULE — generic-safety (read this before touching the kernel)

**The kernel must never hardcode chat/domain vocabulary.** No `'dm'` / `'group'` /
`'reaction'` / `'emoji'` / `'mention'` literals in kernel or messaging *logic*. If a reviewer
asked *"is this a chat backend now?"* the answer must stay **no**.

Domain meaning lives in **three** places, never the kernel:
1. **OPA policy** (the policy plane — `DevOpaPort` now, real OPA later). The DM-cap,
   group-admin, governed-join, media-limit, and notification-trigger rules live here.
2. **Opaque thread/interaction attributes** the kernel stores but never interprets:
   `thread.metadata.membership` (`'dm'|'group'`), interaction annotations (opaque
   `annotationType` + `value` → the app writes `reaction`/`pin`/`save`), `mentions[]` (an
   opaque userId notify-list the kernel fans out; it never parses `@`).
3. **The app** (chat-web) — rendering + product semantics.

Reading an opaque attribute inside a **policy/notification gate** (e.g. `membership === 'dm'`
in `DevOpaPort` or `notification.projector.ts`) is allowed — that file *is* the policy plane,
not the kernel. Everywhere else, keep it generic. Verify with a grep before committing:
`grep -rniE "'dm'|'group'|reaction|emoji" packages/iios-service/src | grep -v spec` — hits
should only be in the policy/notification/app-facing layers or comments.

`pnpm boundary` enforces the layer dependency law (specializations import the kernel, never
the reverse). Run it; don't break it.

## Architecture

- **Kernel primitives** (generic): `IiosScope` (six-vector: org/app/tenant/bu/…), `IiosSourceHandle`
  (externalId = userId; stays `UNVERIFIED` until MDM resolves → `canonicalEntityId`), `IiosActorRef`,
  `IiosThread` (subject/metadata), `IiosThreadParticipant`, `IiosInteraction` (+ `parentInteractionId`
  reply link), `IiosMessagePart` (media as `contentRef`), `IiosInteractionAnnotation` (generic).
- **Platform ports** (`IiosPlatformPorts`, DI token `PLATFORM_PORTS`; dev = `LocalDevPorts`):
  session, opa, cmp (consent), mdm, sas, capability — plus a `StoragePort` (media) and
  `NotificationPort` (push). **Dev stubs → real adapters with zero consumer changes.** Every
  op passes `decideOrThrow(ports, {action,…})` **fail-closed**.
- **Session (auth):** `SessionVerifier` verifies (a) real OIDC tokens (Supabase/`AUTH_ISSUERS`)
  against the issuer JWKS (ES256, no secret), routed by `iss` → per-issuer `appId` scope; or
  (b) legacy dev HS256 app tokens (`APP_SECRETS`, keyed by `appId`). `userId = email` for OIDC.
- **Events:** transactional outbox → `OutboxBus` → **projectors** (inbox, notifications).
  Projectors are **idempotent** (`claim()` on `IiosProcessedEvent` + projection cursor).
  Delivery is at-least-once → clients dedupe by message `id`.
- **Scope isolation:** every row is tagged by `scopeId` (org+app+tenant). By-id ops call
  `assertOwns` (tenant fence → 403). Always `select`/scope Prisma queries.

## Tech stack

NestJS 11 · Prisma 6 / PostgreSQL 16 (docker `iios-db` on **:5434**, db `iios`) · Redis
(socket.io adapter, multi-replica) · socket.io (`/message` namespace) · Vitest · pnpm
monorepo (`packages/*`). `iios-service` is a **modular monolith** (HTTP + WS + relay +
projectors in one process).

## Running & testing

```bash
docker start iios-db                       # Postgres :5434 (OrbStack; `open -a OrbStack` if down)
pnpm --filter @insignia/iios-service exec nest build
# run (dev auth via Supabase; media + notifications enabled):
SUPABASE_URL=https://<ref>.supabase.co REDIS_URL=redis://localhost:6379 PORT=3200 \
  APP_SECRETS='{"portal-demo":"dev-secret"}' MEDIA_DIR=/tmp/iios-media \
  VAPID_PUBLIC_KEY=… VAPID_PRIVATE_KEY=… VAPID_SUBJECT=mailto:dev@insignia \
  node packages/iios-service/dist/main.js   # → :3200 ; GET /health
```

- **`pnpm test`** — Vitest. Runs against an **isolated `iios_test` DB** (globalSetup creates +
  migrates it; `DATABASE_URL` overridden). **It never wipes the dev `iios` DB.** ~205 tests.
- **TDD**: write the failing spec first (see `*.spec.ts` next to the code). DB specs use
  `resetDb()` + real Postgres.
- **Flaky `outbox/replay.spec`**: it's clock/ordering-sensitive and pre-existing. If it fails
  in a full run, `docker exec iios-db psql -U iios -d postgres -c "DROP DATABASE IF EXISTS iios_test WITH (FORCE)"`
  then re-run — it's stale test-DB state, not a regression.
- **`pnpm boundary`** — import-boundary check (must stay OK).
- Smokes: `packages/iios-service/scripts/smoke-*.mjs` (run against a live service).

## Conventions

- **Commits:** conventional (`feat:`/`fix:`/`docs:`/`chore:`), git email
  **`maaz@insigniaconsultancy.com`**, and co-author every commit with Claude. Branch off `main`
  before committing if asked; otherwise the session has committed directly to `main`.
- **⚠️ `IIOS_DEV_TOKENS` MUST be `0`/unset in production** — it exposes `/v1/dev/*` (unauth token
  minting). Single most important prod flag.
- New kernel capability = a **generic primitive** only (see the #1 rule). Add domain meaning in
  policy + app.
- Prisma: always `select` to avoid leaking `passwordHash`/PII; org/tenant scope every `where`.

## What's built (state)

P0–P8: kernel → messaging → inbox → support → adapters → routing → AI → calendar/meetings.
Recent (the "P9 real-providers" era, mostly driven by the chat app):
- **Real Supabase auth** — multi-issuer JWKS verification (`SessionVerifier`); first stubbed
  port turned real.
- **Reactions / pins / saves** — one generic `IiosInteractionAnnotation` primitive
  (opaque type/value), `annotate` socket event, `GET /v1/threads/my-annotations`.
- **@mentions → Inbox** — `mentions[]` on send → `MENTION` inbox item (projector).
- **Media** — `StoragePort` (dev local disk → prod S3/Supabase), presigned upload/download,
  attachment on `MessageDto`.
- **Notifications** — presence-gated Web Push: `NotificationProjector` (policy/presence/mute
  gates) + swappable `NotificationPort`, `focus_thread` presence signal, per-thread mute.
- `senderId` (stable externalId) on messages for reliable "is this mine?".

## Docs (as-built)

- `docs/IIOS_API_AND_SDK_GUIDE.md` — the **as-built REST/socket/SDK reference** (endpoints,
  shapes, env, vocab). Keep it current when adding endpoints.
- `docs/DEPLOYMENT.md` — deploy/topology/env/scaling.
- `docs/IIOS_OVERVIEW_FOR_CEO.md` — plain-language capability tour.
