Maison Doclar
Slice 5 operator console · Last updated May 2026
No sections match your search.
Intelligence is the Maison Doclar OS service that runs structured AI conversations with hosts, synthesises outcomes into JSON, and translates free-text feedback into design directives. Calling apps (Nucleus, Relay, Orion, Badges, and others) never talk to Anthropic directly — they call Intelligence with scoped API keys.
Two operators only manage the system: configure conversation types, maintain the app registry, rotate API keys, monitor usage and sessions, test prompts in the sandbox, and verify gateway webhooks. Hosts interact only through magic-link host sessions owned by Intelligence.
All persistent data lives in PostgreSQL schema app_intelligence on the shared Neon cluster. Deployments run on Railway with migrate → seed → server startup.
After login at /, operators reach /admin (redirects to Conversation Types). The sidebar links to Conversation Types, API Keys, App Registry, Usage, Sessions, Prompt Sandbox, and this manual at /manual. Every admin route requires a valid md-session cookie.
Open the service root /. You see a single password field — no email or username. This is intentional: only two operator accounts exist, distinguished by which password you enter.
| Account | Email (internal) | Env variable |
|---|---|---|
| Operator 1 | command@maisondoclar.com | SUPERADMIN_1_PASSWORD |
| Operator 2 | control@maisondoclar.com | SUPERADMIN_2_PASSWORD |
Passwords are set in Railway (or local .env), hashed with bcrypt cost 12 at seed time, and never stored in plaintext in the database.
/manual.On success, the browser receives an md-session HTTP-only cookie (8-hour JWT). Failed attempts return 401 with Invalid password. Sign out from any admin page clears the session and returns you to /.
GET /health — public liveness; returns {"status":"ok"}. Railway uses this to restart unhealthy containers.GET /api/health/ready — operator-only readiness; runs SELECT 1 against Postgres. Returns 401 without a session.The app_registry table lists every OS app that may call Intelligence. Each row has a unique slug (e.g. nucleus, badges), display metadata, and capabilityContext — prose injected into system prompts at runtime so the model knows what that app can do and what data it already holds.
Why capabilityContext matters: Without accurate capability context, the consultant may hallucinate integrations, ask hosts questions the calling app already answered, or omit features the app supports. Update capabilityContext whenever an app ships new capabilities. The model reads this text on every turn — treat it as the ground truth for what the calling app owns.
Edit registry rows at /admin/app-registry. Each entry includes:
| Field | Purpose |
|---|---|
slug | Stable identifier; matches callingApp in usage logs and API key ownership. |
displayName / description | Human-readable labels in admin UI. |
capabilityContext | Injected into system prompts — the most important field for conversation quality. |
baseUrl | Optional app base URL for operator reference. |
webhookUrl | Where Intelligence may POST synthesis results when enabled. |
synthesisWebhookEnabled | Toggle outbound synthesis webhook delivery. |
synthesisWebhookSecret | Shared secret for signing outbound synthesis payloads. |
isActive | Inactive apps cannot receive new API keys. |
Conversation types belong to an app via appId. When Badges calls Intelligence, the Badges registry row’s capabilityContext tells the model what Badges already knows about the host’s brand identity.
Conversation types are the unit of configuration. No prompts live in application code — all system, synthesis, and translation prompts live in conversation_types. Manage them at /admin/conversation-types.
| Field | Purpose |
|---|---|
slug | Stable identifier; API keys scope to slugs; referenced in inter-app API calls. |
displayName | Label shown in admin UI and sandbox dropdown. |
appId | Owning app from the registry; determines callingApp and capabilityContext source. |
systemPrompt | Main consultant prompt; may include {variable} placeholders. |
systemPromptVariables | Explicit list of allowed variable names callers must supply per session. |
synthesisPrompt | Prompt used when closing a session into structured JSON. |
translationPrompt | Optional prompt for stateless revision-note translation via POST /translate. |
synthesisSchema | JSON Schema validating synthesis model output. |
translationSchema | JSON Schema validating translation model output (optional). |
closingSignal | Token the model emits when ready to synthesise (default [READY_TO_SYNTHESISE]). |
maxTurns | Hard cap on host/AI exchanges; reaching the limit forces session close. |
temperamentGuidance | JSON map of host temperament key → extra instructions; DEFAULT key is fallback. |
model | Default Anthropic model for this type (e.g. claude-sonnet-4-5). |
maxTokensPerTurn | Max output tokens per conversation turn. |
isActive | Inactive types reject new sessions with 404. |
createdBy | Operator email or seed for audit trail. |
At session start, callers supply variables which are merged into the system prompt. The runtime also injects {temperament_guidance} from temperamentGuidance based on hostTemperament. Missing required variables are rejected at the API layer with 400 and a missing array.
Use the admin preview to verify placeholder interpolation before saving. Test closing signals and tone in the Prompt Sandbox before promoting changes to production types.
Calling apps authenticate with keys formatted il_ + 32 hex characters. Only a bcrypt hash and 8-character prefix are stored. The full key is shown once at creation in /admin/api-keys — copy it immediately; it cannot be retrieved later.
scopes is a string array of conversation type slugs this key may access. A key cannot start sessions for types outside its scopes (403).
Seed data creates a placeholder key for Badges pre-go-live:
il_badges_placeholder_rotate_before_goliveil_badgesidentity-consultationRotate this key before Badges goes live. The placeholder is documented in seed output and must not remain in production environments.
/admin/api-keys, create a new key for the same app with identical scopes.last_used_at on the key row and filter Usage by calling app.is_active = false on the old key (Deactivate in admin).Never revoke the only active key for a live app without deploying the replacement first. For compromised keys, create and deploy the replacement immediately, then deactivate the compromised key.
/admin/sandbox runs test conversations against Anthropic without creating real host sessions or conversation sessions. Select a conversation type, fill variable inputs, optionally override the model, enter a host message, and click Run Test.
The optional Model override field accepts any Anthropic model ID (e.g. claude-sonnet-4-5). When empty, the sandbox uses the conversation type’s configured model field. Overrides apply only to the current test run — they do not persist to the conversation type record.
POST /api/admin/sandbox/run streams tokens via SSE.prompt_test_runs and logged to usage_log with operation TEST.closingSignal, the UI shows “Closing signal detected”.Leave the host message empty to simulate the opening turn (the sandbox sends a standard opening user message). Use multi-turn testing by continuing the conversation — turn history accumulates until you reset.
Always sandbox-test prompt changes before saving production conversation types. Verify variable interpolation, temperament guidance, closing signals, and max-turn behaviour.
Intelligence owns the host conversation UI at /host/<token>. Calling apps create a link via POST /api/intelligence/host-session after starting a conversation; the host never sees Badges, API keys, or admin surfaces.
Request body:
sessionId — required; must belong to the caller’s API key.brandingConfig — optional object (see below).expiryHours — optional; defaults to 72.returnUrl — optional URL shown after consultation closes.Returns hostUrl, token, and expiresAt. If an active non-expired host session already exists for the conversation session, the existing link is returned.
title — heading on the host page.subtitle — smaller line under the title.closingMessage — shown after the consultation closes.accentColor — gold accent (default #B79F85).Hosts load GET /api/host/:token/session to restore turns. Messages stream via SSE on POST /api/host/:token/message. When the AI emits the closing signal, the stream sends event: close, the session becomes COMPLETED, and synthesis runs automatically in the background.
Expired or invalid links render a graceful expired page — no raw error codes exposed to hosts.
| Symptom | Likely cause | Action |
|---|---|---|
| “This link has expired” | tokenExpiresAt passed or host session status EXPIRED | Calling app must create a new host session via POST /host-session; default expiry is 72 hours. |
| Blank or stuck loading | Invalid token or session deleted | Verify token in host_sessions; confirm conversation session still ACTIVE or COMPLETED. |
| Messages not sending | Session not ACTIVE | Check session status in Sessions admin; closed sessions reject new host turns. |
| Stream stops mid-response | Network drop or Anthropic error | Host UI shows “Connection lost”; refresh page — turns persist in DB. |
| Closing screen but no synthesis | Background synthesis failed | Check server logs for host_auto_synthesis_failed; manually trigger via inter-app /synthesise if session is COMPLETED. |
| Wrong branding | brandingConfig not passed or cached old session | Pass branding on host-session create; existing active sessions reuse prior branding. |
Calling apps (e.g. Badges) use the inter-app API at /api/intelligence/*. Authenticate with Authorization: Bearer il_… — not operator cookies.
| Method | Path | Description |
|---|---|---|
POST | /conversation/start | Create session + opening AI message. Body: conversationTypeSlug, variables, optional externalRef, hostTemperament. Returns 201 with sessionId and openingMessage. |
POST | /conversation/:sessionId/turn | Host message → AI reply. Body: hostMessage. Returns shouldClose when closing signal seen or max turns reached. |
GET | /conversation/:sessionId | Session metadata + full transcript ordered by turnIndex. |
POST | /conversation/:sessionId/synthesise | Run synthesis into structured JSON. Session must be COMPLETED (or already synthesised). |
POST | /translate | Stateless revision-note translation. Body: conversationTypeSlug, inputText, optional context. |
Additionally, POST /complete records a final outcome (DELIVERED, ABANDONED, or EXPIRED) and maps it to session status. Host link creation is documented in Host Surface (POST /host-session).
| Status | When |
|---|---|
| 400 | Missing required body fields; missing prompt variables on start; translation not configured for type; invalid outcome on /complete. |
| 401 | Missing or invalid API key (Authorization header absent or wrong). |
| 403 | Key not scoped for conversation type slug; session belongs to a different key. |
| 404 | Unknown session, conversation type, or inactive type. |
| 409 | Wrong session state — e.g. turn on non-ACTIVE session; synthesise before COMPLETED. |
| 502 | Anthropic returned non-JSON for synthesis or translation; parse failure. |
Rotate compromised keys from Admin → API Keys: create new key, deploy to caller, verify usage, deactivate old key.
/admin/usage provides full Anthropic call analytics. Every model call writes a row to usage_log. Rows are never deleted — use for billing reconciliation and abuse detection.
| Operation | Source |
|---|---|
CONVERSATION | Inter-app turns and host-surface message streams. |
SYNTHESIS | Structured JSON extraction when a session closes. |
TRANSLATION | Stateless POST /translate calls. |
TEST | Prompt Sandbox runs (/api/admin/sandbox/run). |
Each row stores: operation, model, inputTokens, outputTokens, estimatedCostUsd, callingApp, conversationTypeSlug, optional sessionId, and durationMs. Cost is estimated at write time from model pricing tables in application logic.
API: GET /api/admin/usage accepts query params from, to, callingApp, operation, conversationTypeSlug, page, limit. Returns entries, total, and aggregated summary.
After API key rotation, filter by calling app and confirm last_used_at on the new key aligns with expected traffic before deactivating the old key.
/admin/sessions lists every host journey stored in conversation_sessions. Click a row to open the detail panel with full transcript, synthesis result, host session metadata, and per-session usage summary.
| Status | Meaning |
|---|---|
ACTIVE | In progress; accepts host turns via inter-app or host surface. |
COMPLETED | Consultation closed (closing signal or max turns); synthesis may be pending or in progress. |
SYNTHESISED | Structured JSON stored in synthesis_results; webhook may have been delivered. |
ABANDONED | Calling app reported abandonment via POST /complete with outcome ABANDONED. |
EXPIRED | Calling app reported expiry via POST /complete with outcome EXPIRED, or host link expired without completion. |
conversation_turns — roles AI or HOST with monotonic turn_index. Always read transcripts ordered by turn_index.synthesis_results — JSON result, confidence scores, gaps, and webhook delivery status.host_sessions — magic-link tokens (il_host_…), branding, expiry, and access timestamps.API: GET /api/admin/sessions (list with filters) and GET /api/admin/sessions/:id (full detail). Support workflows: locate session by externalRef (caller-supplied ID) or by session UUID from logs.
POST /api/webhooks/gateway receives events from Maison Doclar Gateway. Requests must include header x-gateway-signature — HMAC-SHA256 hex digest of the raw request body using GATEWAY_WEBHOOK_SECRET.
Every payload is logged to gateway_webhook_logs and acknowledged with 200 immediately; processing runs asynchronously. Invalid signatures return 401.
Intelligence does not own guest data — receipts are logged and processed_at is set on matching log rows. Unknown types are logged as warnings but still return 200.
| Event type | Description |
|---|---|
guest.create | New guest record created in Nucleus. |
guest.updated | Guest profile or metadata changed. |
guest.delete | Guest removed from an event. |
rsvp.updated | RSVP status changed for a guest. |
attendance.reconciled | Attendance counts reconciled post-event. |
checkin.created | Guest checked in at venue. |
Payloads include nucleusGuestId and nucleusEventId when applicable. Query gateway_webhook_logs for audit and replay investigation.
| Symptom | Likely cause | Action |
|---|---|---|
| Container exits on boot | Missing env var | Check logs for [Config] FATAL; fill required vars from .env.example. |
/health OK but login fails | Seed not run or wrong password | Run pnpm seed:superadmin; confirm operators in admins table. |
ready returns 500 | DB unreachable | Verify DATABASE_URL, Neon IP allowlist, migrations applied. |
Inter-app 401 | Wrong or missing API key | Confirm Authorization: Bearer il_…; rotate if compromised; check key is active. |
Inter-app 403 | Scope mismatch | Key scopes must include the conversation type slug; session must belong to same key. |
Inter-app 409 on turn | Session already closed | Check status in Sessions admin; start a new session if consultation ended. |
Synthesis 502 | Model returned invalid JSON | Review synthesis prompt and schema; test in sandbox; check Anthropic response in logs. |
| Placeholder key still in use | Badges not rotated pre-go-live | Replace il_badges_placeholder_rotate_before_golive with a production key. |
| Webhook always 401 | Secret mismatch | Align GATEWAY_WEBHOOK_SECRET with Gateway; signature must be raw-body HMAC hex. |
| Prisma “schema does not exist” | Migration not deployed | pnpm migrate on Railway; confirm app_intelligence schema exists. |
| Model hallucinates app features | Stale capabilityContext | Update registry row in App Registry; verify in sandbox with that app’s type. |
| Usage totals look wrong | Date filter or TEST noise | Reset filters; exclude TEST operation for production cost views. |
| Host link expired | 72h default passed | See Host Surface troubleshooting; create new host-session. |
Structured logs go to stdout and optionally Axiom when AXIOM_API_KEY is set. Missing Axiom config never crashes the server. Each admin page links to the relevant manual section via the ? help icon.