<!-- source: openspec/current/platform.md -->

---
title: Platform
status: stable
owner: pavel
audience: [pavel, ai, engineer, architect, founder]
tldr: Rapoport Studio corporate platform — web, studio workspace, Forge CLI, Muse AI, Canvas. Spec-driven operating model.
---

# Platform

> Source of truth for what Rapoport Studio is, how it's organized, and how its parts relate.
> Capability stubs for `web`, `studio`, `forge`, `canvas`, `muse` are linked from this document.

---

## What it is

**Rapoport Studio** is a Moldovan SRL (in formation, MITP applicant) that builds digital platforms and AI-powered infrastructure for businesses, corporations, and government entities.

The studio operates as a **curator of teams**: each engagement gets a hand-picked group of specialists — illustrators, brand strategists, copywriters, frontend specialists — assembled around the specific shape of the problem. Pavel Rapoport is the founder and engineering anchor; the network around him is part of the offering.

**Rapoport Studio (the platform)** is the technical apparatus that makes this operating model work end-to-end: a public site that explains how the studio works and showcases the network, an internal workspace where projects move from intake to delivery, an AI substrate (Muse) that participates in design and development, a CLI (Forge) that executes spec-driven implementation against Linear-tracked work, and a client communication layer built on Telegram (with WhatsApp as secondary).

---

## Vision (one sentence per layer)

| Layer | One sentence |
|---|---|
| **Studio** (the company) | Sells digital platform and AI-infrastructure services through a curated-team operating model. |
| **Web** (\`rapoport.studio\`) | The public face — explains the studio, showcases the network, intakes new projects. |
| **App** (\`app.rapoport.studio\`) | The internal workspace — Pavel's command surface for running every project. |
| **Forge** (CLI) | Turns Linear-tracked OpenSpec proposals into pull requests via Claude. |
| **Muse** (AI substrate) | One AI personality with multiple modes — Architect (v1) and Builder (v1.2 partial — RAP-48 Waves 1–3) active; Scout and Canvas deferred. |
| **Canvas** (visualization layer) | Live domain-mapping during client conversations and project shaping — currently deferred. |
| **Telegram bot** | Per-client group chat as operational hub — notes and tasks flow into Linear automatically. |

---

## Brand boundaries

The platform's only brand is **Rapoport Studio**, served on `rapoport.studio` (public marketing) and `app.rapoport.studio` (internal workspace). Service marketing, network, intake, and internal workspace all live here.

Anything personal — engineering portfolio, blog, founder biography — is a separate concern in a separate repository and is out of scope for this document.

Critical rule: the studio site does **not** center on Pavel. He's named as founder, but the page that explains "how we work" describes the curation model, not Pavel's biography. The network of collaborators — with their own portfolios and credits — is treated as a first-class brand asset.

---

## Repository

| Field | Value |
|---|---|
| GitHub | \`rapoport-studio/rapoport.studio\` |
| Visibility | Private (open-core extraction for Canvas considered later) |
| Package namespace | \`@rapoport-studio/*\` |

Single monorepo. Two apps, multiple shared packages. OpenSpec lives at the **root** of the repo (it describes the whole project, not any single package).

\`\`\`
rapoport.studio/                      ← repo root
├── apps/
│   ├── web/                          → rapoport.studio (marketing + intake)
│   └── studio/                       → app.rapoport.studio (internal workspace)
│
├── packages/
│   ├── ui/                           Design system implementation (tokens, primitives, blocks)
│   ├── db/                           Supabase clients, types, migrations
│   ├── auth/                         Magic-link sessions + middleware proxy
│   ├── config/                       Typed env + per-vendor accessors
│   ├── forge/                        CLI: OpenSpec + Linear + Claude executor
│   ├── i18n/                         Locales (en, ro, ru)
│   ├── canvas/                       (deferred) Visualization layer
│   └── muse/                         (deferred) Agent definitions, prompts, tools
│
├── tools/                            Internal scripts (deploy, secrets sync)
│
└── openspec/
    └── current/                      Active capability specs (this file is one)
        ├── platform.md               ← this document
        ├── web.md
        ├── studio-workspace/             (split — see studio-workspace/README.md)
        ├── forge/                        (split — see forge/README.md)
        ├── canvas.md
        ├── mirror.md
        ├── muse.md
        ├── design-system.md
        ├── design-tokens.md
        ├── design-motion.md
        ├── design-components.md
        ├── ui.md
        ├── home/                         (split — see home/README.md)
        ├── auth.md
        ├── db/
        ├── config.md
        ├── i18n.md
        └── state.md
\`\`\`

---

## Domain & URL architecture

### \`rapoport.studio\` — public marketing (worker \`rapoport-web\`)

| Route | Purpose |
|---|---|
| \`/\` | Landing — what we do, how we work, primary CTA |
| \`/work\` | Case studies — each shows the team that delivered it |
| \`/network\` | The collaborator roster — illustrators, designers, strategists, writers |
| \`/services\` | Services offered + how we engage |
| \`/lab\` | Long-form content, experiments, articles |
| \`/contact\` | Brief / inquiry form |

### \`app.rapoport.studio\` — internal workspace (worker \`rapoport-studio\`)

Routes marked **v2** are documented for reference; only **v1** routes exist in code today. See \`studio-workspace/spec.md § Surface area\` and \`studio-workspace/narrative.md § Out of scope (until v2)\` for full detail.

| Route | Status | Purpose |
|---|---|---|
| \`/login\` | v1 | Magic-link sign-in (public) |
| \`/auth/callback\` | v1 | Supabase Auth callback for magic-link (public) — see \`auth.md\` |
| \`/\` | v1 | Dashboard |
| \`/canvases\` | v1 | Canvas list with filters |
| \`/spec/[id]\` | v1 | Architect mode chat + artifact panel |
| \`/spec/[id]/turn\` | v1 | SSE turn route handler — Muse runtime stream |
| \`/tasks/[key]\` | v1 | Task detail with embedded Forge pipeline (platform_owner only) — see \`studio-workspace/spec.md § Task page\` |
| \`/tasks/[key]/stream\` | v1 | SSE handler for live Forge run updates (platform_owner only) |
| \`/settings\` | v1 | Profile, API key, appearance |
| \`/token-check\` | v1 (dev) | JWT/cookie inspector — dev-only diagnostic, route group \`(dev)/\` |
| \`/api/studio/intake\` | v1 | n8n bridge for Telegram bot intake (\`/note\`, \`/task\`, reactions). API-key auth |
| \`/api/studio/command\` | v1 | n8n bridge for Telegram/WhatsApp routing. API-key auth |
| \`/api/studio/leads\` | v1 | Lead capture from web \`/contact\` (forwarded by \`/api/intake\` on \`rapoport.studio\`). API-key auth |
| \`/checkout/discovery/[token]\` | v1 | Public Stripe Checkout for the Discovery engagement. Token-gated; excluded from the auth wall. \`X-Robots-Tag: noindex\` |
| \`/checkout/discovery/success\` | v1 | Post-payment success landing (public) |
| \`/checkout/discovery/cancelled\` | v1 | Cancelled checkout landing (public) |
| \`/api/studio/discovery-tokens\` | v1 | Admin: issue/reuse a checkout token for a qualified lead. \`platform_owner\` only |
| \`/api/webhooks/stripe\` | v1 | Stripe webhook handler (\`checkout.session.completed\`). Signature-verified; excluded from the auth wall |
| \`/inbox\` | v2 | New project intakes (from web \`/contact\` and Telegram intake) |
| \`/projects/[slug]\` | v2 | Project workspace |
| \`/clients\` | v2 | Client roster + Telegram group mappings |
| \`/network\` | v2 | Collaborator management (matched against \`/network\` public page) |
| \`/costs\` | v2 | Token usage, billing, cost-per-project breakdown |

---

## Hard rules

Architecture invariants that apply platform-wide. All capability specs and all code changes must respect these.

- **URL = state of UI.** Every meaningful UI state must be reachable by URL — tabs, filters, open dialogs, wizard steps, deep-linked edit forms. Overlays are no exception. Bot-reply deep-links from Telegram must use the same overlay URL grammar. Full contract in `openspec/current/linkable-ui.md`.

---

## Capability map

Each capability has its own spec. This document references them; deep design lives in each linked file.

| Capability | Spec | Status | Notes |
|---|---|---|---|
| Platform | \`openspec/current/platform.md\` | Active | This document |
| Web | \`openspec/current/web.md\` | Active | |
| Studio (workspace) | \`openspec/current/studio-workspace/README.md\` | Active | v1 implementation-ready (Architect mode surface, four routes, authoring flow); v2 expansion deferred per \`ROADMAP.md\` |
| Forge (CLI) | \`openspec/current/forge/README.md\` | Experimental | Eight stabilization blockers — see \`forge/verifier.md § Known gaps\` |
| Canvas | \`openspec/current/canvas.md\` | Deprecated | Replaced by Mirror 2026-05-03; v1 canvas surface (\`canvas_sessions\`, \`/spec/[id]\`) is a separate, active naming — see \`canvas.md § Naming clarification\` |
| Mirror | \`openspec/current/mirror.md\` | Active | Schema + loader landed RAP-32; render pipeline RAP-33, authoring tool RAP-34 |
| Muse | \`openspec/current/muse.md\` | Active | Architect mode active in v1; Builder mode active in v1.2 (RAP-48 Waves 1–3 shipped via PRs #161/#166/#186; \`status\` and \`cancel\` shipping under \`openspec/archive/2026-05-09-sync-forge-builder-spec/\`). Scout and Canvas deferred per \`ROADMAP.md\` |
| Design System | \`openspec/current/design-system.md\` | Active | Restored by change \`restore-design-system-spec-files\` (RAP-1166). Four-file structure on disk: \`design-system.md\` (umbrella visual intent + WCAG 2.2 AA contract), \`design-tokens.md\` (token registry), \`design-motion.md\` (motion spec), \`design-components.md\` (component intent). \`ui.md\` is the implementation-reference layer beneath them. The ratified convention remains at \`_methodology/design-system-convention.md\`. |
| Auth | \`openspec/current/auth.md\` | Active | |
| Config | \`openspec/current/config.md\` | Active | |
| DB | \`openspec/current/db/README.md\` | Active | Types stub; regen against live project pending |
| i18n | \`openspec/current/i18n.md\` | Active | Catalogs empty until consuming features fill them |
| UI | \`openspec/current/ui.md\` | Active | Token reconciliation in flight — see \`ui.md § Known gaps\` |
| Home | \`openspec/current/home/README.md\` | Active | All 11 blocks (Nav, Hero, What we build, Now building, How we work, The network, Services, Lab preview, Closing CTA, Sticky CTA, Footer) implemented; \`/work/[slug]\` and \`/lab/[slug]\` v1.x-deferred per \`web.md § Routing\` |
| State | \`openspec/current/state.md\` | Active | XState v5 primitives library (\`@rapoport-studio/state\`) — \`serverActionActor\`, \`supabaseRealtimeBridge\`, \`makePersistedActor\`, \`attachInspector\`. Consumed by \`apps/studio\` canvas + templates surfaces. Domain machines stay route-colocated; only primitives live here. Landed RAP-128 |
| Connections | \`openspec/connections/\` (per-kind specs) + \`db.md § Project Connections\` | Active | Registry-driven Project Connection Entity (\`packages/connections\`) replaces the per-service columns on \`projects\`. v1 ships GitHub + Linear kinds; new kinds plug in by adding one TS file under \`packages/connections/src/kinds/<slug>.tsx\` + one row in the DB CHECK + one folder under \`openspec/connections/<slug>/\`. All HTTP traffic flows through \`@rapoport-studio/forge/core\` clients (D-PC-13). Landed RAP-134 epic (Phases 1–7, 2026-05-08). **Audit log + disconnect cleanup shipped** (add-connection-audit-and-cleanup 2026-05-13): `connection_events` append-only table, per-kind `onDisconnect` hooks, `disconnectConnectionAction` with full cleanup flow, audit log UI at `/projects/[slug]/connections/[id]`. |
| Telegram | `openspec/current/telegram.md` | Active | Per-spec Telegram surface with two-bot identity split (`@rapoport_studio_bot` for Pavel-bot Linear journal; `@rs_canvas_bot` for canvas Muse running headlessly via `runMuseHeadless`). Engagement opt-in via `projects.tg_surface_enabled`. Landed via `add-tg-canvas-surface` (RAP-121, applied 2026-05-09). |
| Linkable UI | `openspec/current/linkable-ui.md` | Active | URL-state overlay system — `OverlayHost` dispatcher, `DialogModule` contract, `?dialog=` / `?sheet=` URL grammar, lazy-registry pattern. Every meaningful UI state (dialogs, wizards, deep-linked forms) encodes in the URL so it survives refresh, share, and login redirect. Landed RAP-302/303/304 (OverlayHost + studio/web mounts); reference module RAP-305. |
| Robot Operator Surfaces | `openspec/current/robot-operator.md` | Deferred | Three-layer architecture (Gateway / UI Shell / AI-Companion) for bespoke robot operator surfaces. Six MVP transports (Foxglove WS+MCAP, VDA 5050, MAVLink, DJI Cloud API, Spot SDK gRPC, HTTPS REST adapter). Seven UI templates; AI-Companion via `@rapoport-studio/muse/companion`. Package: `packages/operator-shell` (deferred stub). RAP-1151. |

### Status legend

| Status | Meaning |
|---|---|
| **Active** | Spec mirrors current implementation and is reviewable as design source-of-truth. |
| **Experimental** | Implementation exists but has known stabilization blockers; not yet production-stable. |
| **Stub** | Spec fixes surface area and core flows but defers detailed design to a later change. |
| **Deferred** | Vision is fixed; implementation is intentionally not in flight. |
| **Deprecated** | Vision was retired in favor of a successor capability. Spec retained as a tombstone redirect. |
| **TBD** | Capability acknowledged in the map but no spec file yet. |

---

## Stack

| Layer | Tech |
|---|---|
| **Apps** | Next.js 16, React 19, TypeScript 5, Tailwind CSS 4 |
| **Auth** | Supabase Auth (magic-link; \`pavel@rapoport.studio\` auto-admin) |
| **Database** | Supabase PostgreSQL (project \`nifagnmgwoqkplegsicy\`, eu-central-2) |
| **Storage** | Supabase Storage (creative assets, brand assets) |
| **Build/runtime** | Cloudflare Workers via \`@opennextjs/cloudflare\` |
| **CI/CD** | GitHub Actions, secrets via Infisical sync |
| **Secrets** | Infisical (org "Rapoport Studio SLR", project "rapoport-studio", instance \`eu.infisical.com\`, workspace ID \`7d67e5e8-3693-487d-bb18-9fbcb288c96e\`) |
| **AI** | Anthropic Claude API (Sonnet 4.x for routine, Opus for deep work) |
| **Project tracking** | Linear (workspace \`AI Development Studio\`, prefix \`AI-\`) |
| **Code hosting** | GitHub (org \`rapoport-studio\`) |
| **Client comms** | Telegram Bot API (primary), WhatsApp Cloud API (secondary, existing) |
| **Bridges** | n8n Cloud (Telegram/WhatsApp → studio API) |

---

## Bot identities (applied 2026-05-05 from change `add-github-bot-and-creds`)

Forge and other programmatic actors operate under three dedicated external identities, distinct from Pavel's personal credentials. Local development continues to use Pavel's personal PATs; bot identities are production-only. RAP-48's `ForgeIdentity` loader (shipped via `packages/forge/src/core/auth.ts`) provides the local fallback — see `forge.md § Builder mode › Identity model`.

| Identity | Account | Infisical secret | Notes |
|---|---|---|---|
| **GitHub** | `rapoport-studio-bot` (org Member, not Owner) | `GITHUB_BOT_TOKEN` | Fine-grained PAT, 90-day expiry, scoped to all org repos with `Contents` / `Pull requests` / `Issues` read-write only. **No `Administration` permission.** |
| **Linear** | `hello@rapoport.studio` (workspace Member, not Admin) in workspace `rapoport-studio-slr` | `LINEAR_BOT_API_KEY` | Personal API key, default scope. |
| **Supabase** | Project `nifagnmgwoqkplegsicy` service role | `SUPABASE_SERVICE_ROLE_KEY` | RLS-bypass; consumed by `apps/studio/src/lib/supabase-admin.ts` via `getCloudflareContext().env`. |

All three secrets are seeded in Infisical (workspace `7d67e5e8-3693-487d-bb18-9fbcb288c96e`) across `production` / `preview` / `development` environments and applied to both Cloudflare workers (`rapoport-studio`, `rapoport-web`) via `wrangler secret put`.

**Operator gotcha.** `opennextjs-cloudflare deploy` strips worker secrets on every deploy. The three `wrangler secret put` commands must be reapplied to both workers after every `pnpm deploy`. See [CLAUDE.md § Known environment gotchas](../../CLAUDE.md).

**Verification.** `infisical run -- pnpm tsx tools/verify-bot-tokens.ts` probes GitHub `/user`, Linear `viewer`, and Supabase REST. Three `[ok]` lines and exit 0 mean the identity surface is healthy.

**Rotation.** Full setup runbook (account creation, PAT scopes, rotation procedure, Cloudflare wiring) lives at [`openspec/archive/2026-05-05-add-github-bot-and-creds/design.md`](../archive/2026-05-05-add-github-bot-and-creds/design.md) § Setup runbook. The GitHub PAT is the only scheduled rotation (every 90 days); the other two rotate on exposure.

---

## Client communication architecture

> Section rewritten 2026-05-09 from change `add-tg-canvas-surface` (RAP-121). The pre-2026-05-09 version described per-client TG groups + slash-commands (`/note`, `/task`, `/done`, `/status`, `/search`, `/link`) on a single bot identity. Per-canvas TG groups + two-bot split + propose-only mutations supersede that model. Pre-rewrite content preserved in `archive/2026-05-09-add-tg-canvas-surface/draft-amendments/platform.md § Edit target`.

### Two TG bots, two architectural meanings

| Bot | Endpoint | Scope |
|-----|----------|-------|
| `@rapoport_studio_bot` | `/api/studio/command` | "Digital Pavel" — standalone Claude agent. Pavel-only journal: `/note`, `/task`, `/done`, free-text mention. Routes to Pavel's personal Linear team only. NOT Muse. |
| `@rs_canvas_bot` | `/api/studio/telegram/webhook` | Per-spec Muse surface. One spec = one TG group. Voice + text input. Propose-only mutations gated by spec owner. IS Muse running headlessly. |

### Per-spec group as Muse work surface

Every active spec with `tg_surface_enabled = true` (project-level opt-in flag) gets
its own Telegram group:

```
"Spec · {Spec Slug}"
  ├── Spec owner
  ├── Spec collaborators (specialists, contractors, internal team)
  └── @rs_canvas_bot
```

Memberships mirror `project_members`. **Clients are NOT in spec TG groups in v1**
— their layer is Studio UI per `add-client-role`.

The bot is **proactive** in canvas groups (D-TCS-3): smart-listener (per-message
Haiku classification gate before Sonnet call), reminders surfaced from Linear,
optional standup-style nudges (A3 mode, opt-in per spec).

### Voice + text parity

Voice notes flow through Whisper → text → same Muse pipeline as typed input.
Audio retained in R2 for 30 days as reference; transcript is canonical.

### Propose-only mutations

All spec mutations from TG (Mirror writes, GitHub PRs/branches/commits/comments,
Architect-emit `update_proposal/design/tasks`) execute as proposals. Owner approves
via `/yes` from TG; service role then runs the original tool. Read tools (Linear
queries, GitHub reads, web search, OpenSpec readers) execute directly.

Bot replies that include Studio UI deep-links — e.g., a link to open a specific dialog after a mutation is approved — must use the overlay URL grammar (`?dialog=<id>` / `?sheet=<id>`) defined in `openspec/current/linkable-ui.md`. Constructing ad-hoc query strings or hash fragments outside this grammar is forbidden; the bot is a consumer of the URL-state system, not an exception to it.

### Engagement opt-in

`projects.tg_surface_enabled` (default `false`) gates per-spec TG access at the
project level. Charter records the same value as methodology in
`rapoport-studio-engagement.md § 8 Appendix B → D11`. Worker rejects any chat_id
whose spec's project has `tg_surface_enabled = false`.

### What's gone (vs. previous version of this section)

- Per-CLIENT TG group pattern. Replaced by per-spec. Multi-spec clients have
  multiple groups (one per spec), not one group per client.
- Slash-commands `/note`, `/task`, `/done`, `/status`, `/search`, `/link` are
  now Pavel-bot-only commands (route `/api/studio/command`). They are NOT
  available on `@rs_canvas_bot` — the spec surface uses free-text + voice
  with classification gating, not slash-commands.
- Reactions (📝, ✅) — out of scope on spec-bot v1 (could return as Q-TCS-D).

### Why Telegram primary

- **Moldova reality**: Telegram is the dominant business messenger locally; WhatsApp adoption is low.
- **Bot in groups**: Telegram bots work fully in groups without 24h windows or mention-only restrictions.
- **Cost**: Telegram Bot API is free; WhatsApp Cloud API charges per conversation.
- **Setup**: Telegram bots are created instantly via `@BotFather`; WhatsApp requires Meta verification and template approval.
- **Mini Apps**: Telegram supports embedded JavaScript apps inside chats — opens a future surface for in-chat dashboards or approvals.

### Capability spec

Full surface details in `telegram.md`.

---

## Operating model: how a project moves through the system

\`\`\`
1. Lead arrives
   ├─ via web /contact form        → row in studio inbox
   └─ via referral / direct        → manual row in studio inbox

2. Pavel reviews in /inbox
   ├─ Accept → create Project
   │           Create Linear project
   │           Create Telegram group + invite client + bot
   │           Bot links group to Linear project
   └─ Decline → templated response

3. Project shaping (curation)
   ├─ Pavel selects collaborators from /network
   ├─ Brief shared in Telegram group
   ├─ Notes and decisions flow → /note → Linear backlog
   └─ Domain map (Canvas) — when Canvas capability ships

4. Specs and execution
   ├─ Linear issue + linked OpenSpec proposal
   ├─ Forge (CLI) generates PR from approved proposal
   └─ Human review → merge → deploy

5. Delivery
   ├─ Project marked delivered
   ├─ Case study draft in /work (with team credits)
   └─ Telegram group archived or kept for ongoing relationship
\`\`\`

---

## Decisions captured

| # | Decision | Rationale |
|---|---|---|
| D1 | Subdomain split: \`rapoport.studio\` (marketing) vs \`app.rapoport.studio\` (workspace) | SEO isolation, performance profile, auth boundary, independent deploys |
| D2 | Brand split: studio vs Pavel-the-person | Two audiences, two narratives — keep them separate |
| D3 | Studio philosophy: curation, not "Pavel + freelancers" | Differentiates from agencies and freelance platforms; positions network as asset |
| D4 | OpenSpec at repo **root**, Forge as \`packages/forge\` | Specs describe the whole project; Forge is one tool that consumes them |
| D5 | Forge status: \`experimental\` | Honest about current state; stabilization roadmap lives in \`forge.md\` |
| D6 | Canvas and Muse: deferred capabilities, not removed from vision | Vision intact, focus stays on foundation + intake/workspace first |
| D7 | Telegram is primary client channel; WhatsApp secondary | Local market reality; better bot capabilities; lower cost |
| D8 | Per-client Telegram group is the operational hub | Notes/tasks flow into Linear via explicit triggers; group bridges client and team |
| D9 | New repo \`rapoport-studio/rapoport.studio\`; old repo becomes personal-only | Clean code split mirrors clean brand split |
| D10 | Single Supabase project serves both apps | DB is one source of truth; RLS handles auth boundary |
| D11 | Package namespace finalized as \`@rapoport-studio/*\` (full form) | All packages on disk use full form; short-form \`@studio/*\` deferred indefinitely |

---

## Out of scope (for the platform foundation change)

- Implementation details of \`canvas\` and \`muse\` capabilities (deferred — separate proposals later)
- Linear team prefix change (\`AI-\` stays — renaming would break too many cross-references)
- GitHub org rename (\`rapoport-studio\` is already correct)
- Open-source extraction of Canvas (considered, not scheduled)
- Translation depth for long-form content beyond English (chrome ships in en/ru/ro at launch; case studies and articles stay English-only until traffic justifies translation)

---

## Glossary

| Term | Meaning |
|---|---|
| **Studio** | The company (Rapoport Studio SRL). Also: the internal workspace app at \`app.rapoport.studio\`. |
| **Web** | The public marketing site at \`rapoport.studio\`. |
| **Forge** | The CLI in \`packages/forge\`. Turns OpenSpec proposals + Linear issues into PRs via Claude. |
| **Muse** | The AI substrate. Architect mode active in v1; Builder mode active in v1.2 (RAP-48 Waves 1–3). Scout and Canvas deferred. Four modes total: Canvas, Scout, Architect, Builder. |
| **Canvas** | A visualization layer (currently deferred). Domain maps that grow live during client conversations. |
| **Network** | The roster of curated collaborators. Public-facing on \`/network\`, managed in \`/app/network\`. |
| **Curation** | The studio's operating model: hand-picked team per project, not fixed staff. |
| **AI orchestration** | The thesis that AI is the architecture layer of a platform, not a feature bolted on. Engagements are platforms with AI as orchestration substrate, not products with AI features. |
| **Orchestra** | The visual diagram on \`/\` and \`/services\` representing the AI-orchestration thesis. Distinct from Muse (agent substrate) and Canvas (visualization layer for live domain-mapping). |
| **Capability** | A first-class concern with its own OpenSpec file. Each capability evolves independently. |
| **Change** | A proposed modification to one or more capabilities, with proposal/design/tasks documents. |
| **OpenSpec audit panel** | The nine-to-eleven-role parallel-agent audit run against an OpenSpec change folder via `forge audit openspec-change <slug>`. Gates Stage 2 → Stage 3 advance per `workflow.md § Stage 2 — Spec`. Defined by `add-openspec-audit-pass`. |
| **Audit verdict** | The `BLOCK | PASS-WITH-FIXES | PASS` outcome emitted by the OpenSpec audit panel's verifier. `BLOCK` halts the workflow gate; `PASS-WITH-FIXES` advances with open Linear sub-issues; `PASS` advances clean. See `add-openspec-audit-pass/design.md § 3.3`. |

---

## Invitation-driven access (applied 2026-05-02 from change `add-canvas-invitations`)

> **Updated 2026-05-15** by change `redesign-canvas-permission-model`. Project-level membership is now the primary access mechanism. Per-canvas direct invitation (`canvas_invitees`) is preserved as a legacy path. See `auth.md § Project member tier` for the complete proxy chain.

The studio-wide email allowlist (`STUDIO_ALLOWED_EMAILS`) introduced in AI-112 has been replaced by a per-canvas invitation model, itself superseded by project-level membership. Access is granted at three levels:

**1. Platform owners** — users carrying `auth.users.raw_app_meta_data.platform_owner = 'true'`. Owners can create canvases, see and access any canvas they own, and invite collaborators. The flag is bootstrap-managed via SQL (no UI in v1) and was seeded for `pavel.rapoport@gmail.com` and `hello@rapoport.studio`. The flag is read from the JWT on every request — no DB roundtrip on the hot path.

**2. Project members** *(primary, as of 2026-05-15)* — users with a row in `project_members` for the project that owns the canvas, with `accepted_at IS NOT NULL`, `revoked_at IS NULL`, and `expires_at` in the future (or null). Project membership cascades to all canvases in the project by default. Six roles: `owner`, `co_owner`, `collaborator`, `external_collaborator`, `viewer`, `client`. The proxy evaluates this in a single indexed query after the `platform_owner` short-circuit and `community_member` branch. Full role matrix: `db/entities.md § Project members + role hierarchy`.

**3. Per-canvas invitees** *(legacy, preserved)* — users with a row in `canvas_invitees` with `accepted_at IS NOT NULL` and `revoked_at IS NULL`. This path is evaluated last by the proxy and RLS (Branch 3). Existing per-canvas invitations continue to work. Orphan canvases without a `project_id` rely exclusively on this path.

**Auto-accept on first login.** The auth callback at `apps/studio/src/app/auth/callback/route.ts` matches the authenticated email against any unaccepted, non-revoked `canvas_invitees` row and stamps `invitee_user_id` + `accepted_at` in a single update. The user lands on their invited canvas directly.

**Access predicate.** RLS on canvas tables uses a three-branch pattern: `platform_owner` short-circuit → `project_members` EXISTS → `canvas_invitees` EXISTS. The middleware short-circuits on `platform_owner = 'true'`; project members pass at step 3; canvas invitees pass at step 4. Full RLS pattern: `db/conventions.md § Project-membership-first RLS pattern`.

**Owner-only server actions.** `inviteToCanvasAction`, `revokeInvitationAction`, and `listCanvasInvitations` gate on `canvases.user_id = session.user_id` for the target canvas — collaborators cannot invite or revoke (legacy direct-invite path). Project-level invitation uses `inviteMember` in `apps/studio/src/app/actions/project-members.ts`.

**Login error path.** When an authenticated user has neither the `platform_owner` flag, nor a project_members row, nor any accepted canvas_invitees row, the proxy signs them out and redirects to `/login?error=no_access`.

**Secret status.** The `config.studio.allowedEmails` accessor and the `AuthProxyConfig.allowedEmails` field were removed from the codebase in Phase 6 of `add-canvas-invitations`. The `STUDIO_ALLOWED_EMAILS` env var has been deleted from Cloudflare Workers (Phase 7); deletion from Infisical is a manual operator step (production environment, workspace `7d67e5e8-3693-487d-bb18-9fbcb288c96e`).

---

## Canvas creation (restricted to platform_owner as of 2026-05-02)

`createCanvasAction` requires `auth.users.raw_app_meta_data.platform_owner = 'true'` before any DB attempt. Server-side guard rejects non-owners with a clean error message; UI hides «New canvas» CTA for non-platform-owners across dashboard and `/canvases` list.

Defense in depth: server guard (security) + UI hide (UX) = clean experience for owners + secure boundary against direct API calls.

Existing orphan canvas owned by `pavel@rapoport.studio` (no platform_owner flag) remains accessible to its owner but no new orphans can be created. Orphan cleanup tracked in RAP-11.

---

## Role-based canvas access (applied 2026-05-02 from change `add-client-role`)

Three effective roles in the canvas surface:

| Role | Source | UI | Artifact access |
|---|---|---|---|
| `owner` | `canvas_sessions.user_id = auth.uid()` | Full + invite controls | Full |
| `collaborator` | `canvas_invitees.role = 'collaborator'` AND accepted | Full minus invite | Full |
| `client` | `canvas_invitees.role = 'client'` AND accepted | Chat only (full width) | None (RLS-blocked) |

Owner is implicit (no canvas_invitees row). `Viewer.role` resolves at request time from canvas owner check + invitee lookup, defaulting to `'collaborator'`.

**Defense in depth:**
- L1 (RLS): `canvas_files` policy excludes clients from invitee branch
- L3 (UI): conditional layout in `canvas-view.tsx` hides artifact panel + promote button for clients

InvitationModal exposes role picker (collaborator default). `<CanvasParticipants>` component in canvas header surfaces all participants with avatars + role-on-hover tooltips.

---

## Agentic Muse + persona system (applied 2026-05-03 from change `add-muse-research-tools-with-personas`)

Muse engine integrates Anthropic's `tool_use` API. Five tools shipped: foundation tier (`web_search`, `fetch_url`, `read_openspec`, `read_archive`) plus specialized tier (`domain_research`). Cost guard: $0.50 per canvas budget. Personas (5 enum values) soft-route tool selection and output format.

UI surfaces: `InvitationModal` persona dropdown, `CanvasParticipants` tooltip extension, `ToolStatusPill` component for streaming feedback, GFM footnote citations.

End-to-end activation pending a separate change — the current `runArchitect` server action uses the synchronous flow; the `runMuseWithTools` async generator is awaiting its consumer.

Detailed behavior in `muse.md § Research tools + persona system`.

---

## Client onboarding pipeline (applied 2026-05-04 from change `add-client-onboarding`)

End-to-end lead pipeline from marketing site to studio inbox to canvas creation.

### Intake flow

Prospective clients submit a brief via `/contact` (full engagement) or `/discovery` (discovery tier, €5k fixed). The `IntakeForm` component posts to `/api/intake` (in `apps/studio`). The API validates, writes to the `inbox` table via service-role client, and returns 201. No auth required from the submitter's side.

### Discovery tier

`/discovery` is a new marketing page introducing the Discovery engagement: €5,000 fixed, 3–4 weeks, NDA-bound. Copy: 4-step process (Map domain → Design AI layer → Validate plan → Deliver spec). Homepage carries a `DiscoveryBanner` between Hero and WhatWeBuild, linking to `/discovery`.

### Studio inbox

`/inbox` in `app.rapoport.studio` is a platform_owner-only surface listing all leads. The detail view at `/inbox/[id]` shows the full lead payload, metadata, and an action bar. Actions:
- **Mark reviewing** — status `new → reviewing`
- **Decline** — status `any non-terminal → declined`
- **Accept →** — opens `CanvasCreateDialog` in `from-inbox` mode; creates a canvas and sends a magic-link invite in two steps
- **Retry invite →** — same dialog, skips canvas creation (uses existing `canvas_id_if_accepted`), retries the invite step

### M6 half-success state machine

`acceptLeadAction` is intentionally non-atomic. Canvas creation and invite delivery are two separate DB writes. If invite delivery fails, the row enters `accepted_pending_invite` with `canvas_id_if_accepted` set — observable in the inbox and retryable. Only after both steps succeed does the row reach `accepted`.

### Sitemap

`apps/web/src/app/sitemap.ts` emits all static marketing routes for all three locales with `alternates.languages` hreflang entries. Routes: `/`, `/contact`, `/discovery`, `/services`, `/work`, `/lab`, `/network`.

---

## Entities capability (applied 2026-05-06 from change `add-entity-system`)

A new active capability joins the map: **Entities** (`openspec/entities/README.md`). The capability owns the canonical registry of every primary domain entity in the platform — Projects, Canvases, Leads, Files, Invitations, Instructions, Templates, Tool Uses, Users, Specialists, Community Members, Capabilities, Changes, plus external integrations.

Each catalog entry locks: stable identifier (en-only, never translated), display-name i18n key, tier (one of seven: platform-structural / domain / concept / network / content / spec / external), backing source of truth (Postgres table, file path, concept-only, or external system), Lucide icon component, optional letter-glyph slot (per the orthogonality rule, only Tier-3 taxonomies populate it), canonical user-visible fields, consuming surfaces.

The catalog is the single source of truth that consumer surfaces (`<InboxRowItem>`, `<WorkCard>`, `<PersonCard>`, KPI tiles, file tree, project home Bindings card, inbox detail, canvas list) consult before composing rows or cards. The TypeScript registry mirroring this file plus the `<EntityCard>` rendering primitive ship under RAP-92 (`add-entity-card-primitive`, deferred); consumer migration follows under `migrate-surfaces-to-entity-card`.

Capability map row:

| Capability | Spec | Status | Notes |
|---|---|---|---|
| Entities | `openspec/entities/README.md` | Active | Catalog of ~38 primary entities across 7 tiers; Lucide locked as canonical icon library; EntityCard contract defined |

---

## Forge UI surface (applied 2026-05-07 from change `add-forge-ui-task-detail`)

The Forge capability gains a UI surface inside `apps/studio`. The CLI engine described in `forge.md § Architecture` stays unchanged — this addition wires Forge into the studio's `(auth-walled)` shell at `/tasks/[linear-key]`.

### Capability map row update

The `Forge (CLI)` row in the capability map now carries a UI bullet. The capability remains `Experimental` (per `forge/verifier.md § Known gaps`); shipping a UI does not stabilize the underlying engine. Effective contract:

> **Forge (CLI)** — `openspec/current/forge/README.md` — Experimental — Eight stabilization blockers — see `forge/verifier.md § Known gaps`. **UI surface** at `/tasks/[linear-key]` ships in `apps/studio` (platform_owner only) — see `forge/entities.md § UI surface`, `studio-workspace/spec.md § Task page`. Five new domain entities catalogued in `entities/persisted.md § Forge entities`; backing schema in `db/entities.md § Forge tables`.

### New domain entities

Five entities (`forge-run`, `forge-step`, `forge-finding`, `forge-tool-call`, `forge-output`) are added to `entities/persisted.md` as Tier-2 (domain) members. They back the Task page and are surfaced through the three new UI primitives in `ui.md § Forge primitives` (`<PhasePipeline>`, `<LiveLogStream>`, `<FindingRow>`).

### New tokens

Four mode-aware severity tokens (`--severity-critical`, `--severity-major`, `--severity-minor`, `--severity-info`) join the design system; their canonical home is the restored design-system corpus — `openspec/current/design-system.md` and the token registry at `openspec/current/design-tokens.md` (RAP-1166). They power the severity-colored `<FindingRow>` primitive.

### Identity reuse

No new identities. The Forge UI server runtime reuses the existing platform bots (`LINEAR_BOT_API_KEY` for read + comment, `GITHUB_BOT_TOKEN` for PR create / merge — see `§ Bot identities`) plus the Supabase service role for run-state writes.

---

## Personas (applied 2026-05-08 from change `add-founder-track`, RAP-98)

The platform recognizes three first-class human personas plus one platform-only persona, settled across two changes (`expand-home-with-services-and-community`, RAP-82; `add-founder-track`, RAP-98):

| Persona | Origin | Distinction |
|---|---|---|
| **Client** | RAP-81 / RAP-82 | Problem-owner buying a curated team. Client does not own the product (the studio delivers it). |
| **Founder** | `add-founder-track` (RAP-98) | Problem-owner who is also product-owner. Bears product-market-fit risk. Five sub-segments F1–F5 (`archive/2026-05-08-add-founder-track/research/personas-founder.md`). |
| **Specialist** | RAP-81 / RAP-82 | Sells expertise into others' projects (engagement clients). Works inside Muse canvas; does not own the product. Trained, NDA'd, certified per `_methodology/specialist-network.md`. |
| **Community member** (platform-only) | `add-studio-self-serve-signup` (RAP-83) | Self-registered platform user; pre-NDA; bounded access; can apply to certified specialist network. |

The triad-plus-one is parallel, not hierarchical. Founder and Client both buy from the studio, but on different funnels (Client Track and Founder Track) and at different commercial floors (€50k+ engagements for Client; existing services with founder framing for Founder, Discovery as the wedge). Specialist is the seller of expertise into both tracks. Community member is the open-access shape that can convert into either Client (by hiring) or Specialist (by applying to certification).

A single human can occupy multiple persona roles across time (a founder who hires a curated team becomes a Client for that engagement; a specialist who launches their own product becomes a Founder for that product). The platform addresses persona-by-persona at the marketing-surface level; in any given engagement only one persona role is active.

**Specialist → `external_collaborator` role mapping (applied 2026-05-15 from change `redesign-canvas-permission-model`).** When a Specialist persona is hired into a specific project engagement, they land as an `external_collaborator` row in `project_members` for that project. The `external_collaborator` role is conventional, not enforced at the persona layer — a specialist who is also an internal collaborator on a different project holds `collaborator` on that project. The mapping is: Specialist persona + active hire = `external_collaborator` project_members row with `expires_at` set to the engagement end date. Full role matrix in `db/entities.md § Project members + role hierarchy`.

---

## Geographic ICPs (applied 2026-05-08 from change `add-founder-track`, RAP-98)

The platform operates two parallel geographic ICPs:

| ICP | Track | Notes |
|---|---|---|
| Moldova + Romania | Client Track | Q-FUNNEL-1 from `archive/2026-05-06-expand-home-with-services-and-community/`. Language priorities en/ru/ro; Pavel's first-degree network. |
| Israel | Founder Track | D-F2 from `archive/2026-05-08-add-founder-track/proposal.md`. Founder Track distribution leverages Pavel's Israeli network and warm-intro density. Hebrew chrome shipped 2026-05-16 (`harden-cookie-compliance-followup`, RAP-906) and was removed the same day — the AI-drafted strings were not worth maintaining ahead of a native-speaker review pass. Hebrew is deferred until a professionally-translated locale is justified by qualified IL intake volume. |
| Cross-geo (en/ru/ro) | Legacy Track | D-L1..D-L9 from `archive/2026-05-22-add-legacy-track/`. Third audience track parallel to Client + Founder — operationalises the `Long (legacy)` motivation pole from `_methodology/positioning.md § 5.1` (dominant for 4 of 9 archetypes A1/A5/A8/S1). Cross-geo funnel matrix per D-L4: RO soft-generational, IL post-cyber, MD patient-durable. Surfaces: `<ForLegacy />` homepage block + `/legacy` route + intake source `web/legacy`. Pricing published (€50k floor / 6-month minimum, Q-PRICE-1 ratified). |

The three ICPs are not segmented at the homepage chrome level — language and CTAs are uniform across all visitors. The ICP distinction lives in funnel design (which audience is the cold-message and content effort biased toward) and in case-study / lab-content pipeline priorities, not in conditional UI.

---

## Cold-pitch vocabulary constraint (applied 2026-05-08 from change `add-founder-track`, RAP-98)

Public surfaces (Hero, ForFounders block, `/founders` hero, contact CTA, and any future founder-facing cold-message surface) MUST NOT use the following tokens:

- `spec`
- `methodology`
- `process`
- `review gate`
- `discipline`
- `framework`

These tokens may appear on `/services` deep-dive pages, in `/lab` long-form content, and in second-meeting / engagement-document materials, but never on the cold-pitch surface. The constraint exists because the Israeli founder ICP's vocabulary register treats methodology vocabulary as bureaucratic overhead, not as quality signal. The studio's credibility move that survives this filter is the **artifact** itself: the public `openspec/` directory is the credibility move, not the methodology language describing it. See `archive/2026-05-08-add-founder-track/research/israeli-startup-ecosystem.md § C` for the source ethnographic read; `archive/2026-05-08-add-founder-track/research/cold-pitch-language.md` for the approved-vocabulary table and per-surface candidate copy.

`/founders` adds founder-page-specific forbidden phrasings: "build your dream product", "AI-powered", "10x your runway", "we're your CTO", "fractional CTO", "for ambitious entrepreneurs", "from idea to exit". These are checked at PR review time during implementation.

The trust-pillar canonical sentences (`Spec is markdown in your repo. From day one. Every commit is yours.` / `Studio never writes code in your repo. Specialists never receive prod access.` / `Close the engagement. Take the spec. It still works.`) are pinned verbatim from `archive/2026-05-06-expand-home-with-services-and-community/research/muse-trust-and-ownership.md` and are explicitly exempt from the lone-`spec` token rule — they reference OpenSpec as a proper noun.

The ESLint rule for forbidden tokens now ships and is CI-enforced (applied 2026-05-22 from change `wire-spec-enforcement-gates`, RAP-1167). The local rule `tools/eslint-rules/no-cold-pitch-tokens.js` lints the i18n message JSON backing the four cold-pitch namespaces (`web.home.hero`, `web.home.forFounders`, `web.founders.hero`, `web.founders.closingCta`) and fails `pnpm lint` — and therefore CI — when any of the six forbidden tokens (`spec`, `methodology`, `process`, `review gate`, `discipline`, `framework`) appears. The trust-pillar canonical sentences are exempted by an exact-string allowlist inside the rule.

## Studio onboarded as first-class project (applied 2026-05-09 from change `migrate-studio-into-platform`, RAP-169)

The studio itself is now a registered engagement in the platform's data plane, on the same shape as external engagements (`unbind`, `vivod`, `dentour`, `i-avocat`, `pavelrapoport-com`). Concretely:

- `public.projects` carries one row `slug='studio', name='Rapoport Studio', status='active'`, owner = Pavel's `auth.users.id` (resolved by email), legacy bind columns (`github_repo`, `github_repo_url`, `linear_team_id`) populated for parity.
- `public.project_connections` carries two `studio` rows: `kind='github'` (`rapoport-studio/rapoport.studio`) and `kind='linear'` (RAP team `c48bbbb1-9eba-477d-87d2-d082c506d885`). Both `active`. The `linear` connection's config gained a runtime-written `teamKey: 'RAP'` field after first refresh — informational, not part of the migration.
- `openspec/brands/studio/identity.md` exists as a placeholder (Tier-2 Mirror shell, fields populated when the first studio canvas runs Mirror updates).
- `openspec/_methodology/projects/studio.yaml` exists with `engagement.mode: full`, `team_prefix: RAP-`, `bot_account: paveliko` (deliberate divergence from external engagements that route through `rapoport-studio-bot`).

Net effect: `app.rapoport.studio/projects/studio` resolves with the Connections card populated. Self-hosting is symmetric — the same canvas → architect → spec → forge plan → forge build → PR loop the studio runs daily for client engagements is now wired for the studio's own work, with the workspace surfaces tying back to a registered project.

The data-substrate part of MVP v1 Track D Cycle 2 is shipped via this change. The dogfood proof — running a real RAP-* issue end-to-end through the UI and observing it in `<ForgeRunsCard>` on `/projects/studio` — is deferred to its own session and tracked as Track D Cycle 2 in `ROADMAP.md`. A skeleton post-mortem at `openspec/_research/studio-self-hosting-first-run.md` captures the pre-dogfood state and is appended to once Cycle 2 runs.

Out of scope for this onboarding (tracked separately): drop of deprecated `projects.github_repo` / `linear_team_id` columns (`drop-deprecated-projects-binding-columns`); Mirror coverage card on `/projects/[slug]` (deferred per `studio-workspace/spec.md § Project routes`); switch of `bot_account` from `paveliko` to `rapoport-studio-bot` for studio commits (D-S? candidate, future ratification).

## Per-user OAuth connections (applied 2026-05-10 from change `add-linear-oauth-connection`)

Linear adds a per-user OAuth binding alongside the bot-token model documented in `## Bot identities`. Three new secrets sit in Infisical (workspace `7d67e5e8-3693-487d-bb18-9fbcb288c96e`) across prod / preview / dev:

- `LINEAR_OAUTH_CLIENT_ID`
- `LINEAR_OAUTH_CLIENT_SECRET`
- `LINEAR_OAUTH_ENCRYPTION_KEY` — AES-256 key for at-rest token encryption (256-bit, base64-encoded)

Mirror to Cloudflare worker `rapoport-studio` via `wrangler secret put` for production. Per memory `opennext_secrets_persist.md`, `@opennextjs/cloudflare` 1.19.4+ preserves secrets across `pnpm deploy` — verify on the live worker rather than blindly re-applying.

**Precedence rule for studio actions touching Linear**: prefer the user's OAuth-bound client when `linear_oauth_connections` has an active row for the requesting user; fall back to `LINEAR_BOT_API_KEY` otherwise. The Telegram bridge follows this rule so issue authorship reflects the actual operator.

Authoritative source: `archive/2026-05-10-add-linear-oauth-connection/{proposal,design}.md`.

## URL-state overlay system cross-references (applied 2026-05-10 from change `add-url-state-overlay-system`, RAP-308)

Added linkable-ui capability row to § Capability map; added § Hard rules with the URL = state of UI invariant; added overlay URL grammar constraint to § Client communication architecture § Propose-only mutations. Full overlay contract in `openspec/current/linkable-ui.md`.

## Workflow timeline (applied 2026-05-12 from change `establish-workflow-spec`)

Workflow timeline applied 2026-05-12 from change `establish-workflow-spec` — see [`openspec/current/workflow.md`](workflow.md) for the eleven-stage operating timeline.

## Manifesto trilingual structure (applied 2026-05-13 from change `extend-philosophy-foundation-trilingual`)

Trilingual manifesto contract archived 2026-05-13. Phases 1 (spec), 3 (EN translation), and 4 (RO translation) complete. Phase 2 (RU canonical body — `openspec/_methodology/manifesto.ru.md`) is operator-blocked, requiring Pavel-voice authorship. EN and RO translation files (`openspec/_methodology/manifesto.en.md`, `openspec/_methodology/manifesto.ro.md`) are structurally present; `translation_status: draft` pending the RU canonical. Full change record: `openspec/archive/2026-05-13-extend-philosophy-foundation-trilingual/`.

## Specialist Network capability (applied 2026-05-13 from change `add-specialist-onboarding`)

Specialist Network moves from methodology-only to a tracked capability. UI, routes, and type stubs ship; DB migrations and first cohort onboarding are operator-blocked.

### Capability map row update

| Capability | Spec | Status | Notes |
|---|---|---|---|
| Specialist Network | `openspec/_methodology/specialist-network.md` | Stub | Landing surface (`/specialists`), intake form (`/specialists/apply`), profile (`/profile/specialist`), and inbox review surface (`/inbox/specialist`) ship as Phase 1–3 of change `add-specialist-onboarding` (PRs #936, #941). DB migrations (`specialist_applications`, `specialists`) are operator-blocked (Supabase project access). Status upgrades to **Active** when first cohort-1 specialist completes NDA + training; promoted to `openspec/current/network.md` at third certified specialist per `specialist-network.md § 0`. |

### Personas note

P0.1 vocabulary decision: **extend Specialist** (default). No new persona row added.
## Project connect — one-button GitHub App install (applied 2026-05-13 from change `add-project-connect-onebutton`)

One-button GitHub App install flow ships as RAP-84. Users land on `/projects` with zero projects, click "Connect a GitHub repo", and are redirected through a GitHub App installation that creates a `project_connections` row and a `projects` row in a single server-side transaction.

### Auth source

Connection auth source: `github-app-install` (see `packages/connections/src/types.ts` `auth.source` union). Per-installation tokens are minted at request time via `loadGitHubAppToken(installationId)` in `packages/forge/src/core/auth.ts` using `@octokit/auth-app` (Workers-compatible — RSA via `crypto.subtle`).

### Install flow

| Step | Route | Notes |
|---|---|---|
| Initiate | `GET /api/studio/github/install/connect` | Mints 32-byte CSRF nonce → HttpOnly `gh_install_state` cookie (5 min) → 302 to `github.com/apps/rapoport-studio/installations/new?state=<nonce>` |
| Callback | `GET /api/studio/github/install/callback` | Validates state cookie, reads `installation_id`, mints App JWT, derives slug from first repo name (kebab, max 60 chars), idempotent `INSERT` on `project_connections` keyed on `installation_id`, inserts `projects` row, 302 to `/projects/<slug>` |
| Webhook | `POST /api/studio/github/install/webhook` | HMAC-SHA256 signature gate before body parse; `installation.deleted` → soft-delete (`status = 'uninstalled'`); other events no-op in v1 |

### Secrets layout

Five secrets added to `apps/studio` Cloudflare Worker (see `wrangler.jsonc` comment block, RAP-84 § Infisical path `/studio/github-app/`):
`GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`.
Operator-provisioning steps: `openspec/archive/2026-05-13-add-project-connect-onebutton/design.md`.

### CTA

Empty-state card on `/projects`: "Connect a GitHub repo" primary `<a>` → `/api/studio/github/install/connect`. Existing slug+name form remains as secondary panel (no i18n keys — studio does not consume `packages/i18n`).

### Cross-references

- Connection record shape: `openspec/archive/2026-05-13-add-project-connect-onebutton/design.md § 3`.
- `packages/connections` kind handler: `packages/connections/src/kinds/github.tsx`.
- Forge token path: `packages/forge/src/core/auth.ts` `loadGitHubAppToken`.
- Phase 2 (App registration, secrets) and Phase 5.5 (E2E smoke test) remain operator-blocked.

---

## Cookieless-by-default posture (applied 2026-05-15 from change `harden-cookie-transparency`)

The studio enforces a **cookieless-by-default** posture on the marketing surface (`rapoport.studio`) as a strategic compliance position covering EU GDPR Art. 13/14 + ePrivacy Art. 5(3), Romania (Law 506/2004), Moldova (Law 133/2011 → Law 195/2024 from 2026-08-23), and Israel (PPL Amendment 13 in force from 2025-08-14). One global posture, no geo-detection, no CMP banner today.

The workspace surface (`app.rapoport.studio`) writes only strictly-necessary session cookies (Supabase auth + PKCE + sidebar state) and functional `localStorage` (mobile-banner dismissal, canvas-stage celebration guards, intake-form drafts).

- Single source of truth: [`_legal/cookie-inventory.md`](../../_legal/cookie-inventory.md).
- Public disclosure: `/legal/cookies` (two-surface) and `/legal/sub-processors` (10 executed vendor DPAs as bridge implementation until `publish-legal-bundle` Phase 6 lands).
- CI gate: [`tools/verify-cookieless-marketing.mjs`](../../tools/verify-cookieless-marketing.mjs) — scans `apps/web/` + `packages/ui/` for forbidden patterns; allowlist exactly one host (`static.cloudflareinsights.com`).
- Forensic map: every code-level write site carries a `cookie-inventory:` comment anchor.
- Adding any non-essential identifier reverts D-CT-4 and ships a CNIL-grade CMP banner — separate change.

Full design + decision history: `openspec/archive/2026-05-15-harden-cookie-transparency/`.

### Follow-up — transparency notice + consent log (applied 2026-05-16 from change `harden-cookie-compliance-followup`)

A first-visit **informational notice** (`<TransparencyNotice>`) now surfaces the cookieless posture proactively — **not a consent gate** (D-CC-1; there is nothing non-essential to gate). Dismissals write to a `consent_events` Supabase table (anon-INSERT-only RLS; forward-compatible schema for a future full CMP) keyed by an anonymous, no-PII `localStorage` UUID. Two new functional `localStorage` identifiers (`cookie-notice-dismissed:v1`, `anon-visitor-id`) are CI-gate-allowlisted via inline directives. See `web.md § Cookie transparency notice`. Full history: `openspec/archive/2026-05-16-harden-cookie-compliance-followup/`.

---

## Unbind registered as a studio project (applied 2026-05-20 from change `register-unbind-in-studio-registry`)

Unbind — the studio's first external sub-product — now has a registry row in the live studio Supabase. `/projects/unbind` resolves with bindings (Linear team `Unbind`/`UNB`, GitHub repo `rapoport-studio/unbind.life`) and Pavel as owner-member. Engagement kickoff itself (D-SE1 mode ratification, bot repo-grant, first cross-repo Forge dogfood) remains pending per `_methodology/projects/unbind-kickoff-runbook.md` §§ 1–5, 8–9.

Source: `openspec/archive/2026-05-20-register-unbind-in-studio-registry/`.

---

## Deploy failure notifications (applied 2026-05-25 from change `add-deploy-failure-notifications`, RAP-DFN)

A GitHub Actions listener (`.github/workflows/notify-deploy-failure.yml` + `notify-deploy-failure.sh`) fires a Telegram alert to the operator's chat whenever `Deploy studio (production)` or `Deploy web (production)` finishes with `conclusion: failure`.

### Alert payload (plain text — no Markdown `parse_mode`)

```
🚨 Deploy failed — <workflow name>

• Run #<n> on <branch> @ <8-char sha>
• Failed step: <step name>
• Error: <first ##[error] or 'Error: ' line, ≤200 chars>
• Run: <html_url>
```

### Triggers

- **`workflow_run: completed`** — listens to both production deploy workflow names; `if:` filter keeps the path conditional on `conclusion == 'failure'`. Cancelled runs (concurrency supersession) silently skip.
- **`workflow_dispatch: { replay_run_id }`** — spy-mode entry-point. Replays the listener against any historical run id (within GitHub's 90-day log retention). Used for first-deploy verification and re-verification after secret rotation. Same alert format with `WORKFLOW_NAME = 'replay'` so spy alerts are distinguishable in chat history.

### Secrets

The listener requires two Telegram secrets in the GitHub Actions secrets store:

- `TELEGRAM_FUNNEL_BOT_TOKEN` — the Telegram bot's HTTP API token.
- `TELEGRAM_OPERATOR_CHAT_ID` — the operator's DM or operator-only group chat id.

**⚠ Operator gate (verified 2026-05-25 via spy-mode run [26384262446](https://github.com/rapoport-studio/rapoport.studio/actions/runs/26384262446)).** Both secrets are referenced in the workflow but were not present in the GitHub Actions secrets store at the time the implementation landed. The spy dispatch surfaced this via the workflow's own `::warning::Telegram send failed; alert not delivered.` annotation — the graceful-fail path is verified working (exit 0, no cascading red, just a missing alert). Provisioning steps:

1. Create or reuse a Telegram bot via `@BotFather`; capture `<bot_token>`.
2. Capture the operator's chat id (`https://api.telegram.org/bot<bot_token>/getUpdates` → `result[].message.chat.id`).
3. `gh secret set TELEGRAM_FUNNEL_BOT_TOKEN -R rapoport-studio/rapoport.studio` (paste token).
4. `gh secret set TELEGRAM_OPERATOR_CHAT_ID -R rapoport-studio/rapoport.studio` (paste chat id).
5. Re-run the spy dispatch (`gh workflow run notify-deploy-failure.yml -f replay_run_id=26374045851`) to confirm the alert lands in the operator's chat.

`GITHUB_TOKEN` (the workflow's auto-provisioned token, `permissions: { actions: read, contents: read }`) reads the failed-run jobs metadata + logs. If a future Actions permission change breaks the cross-workflow log read with a 403, swap the workflow env line to `GH_TOKEN: ${{ secrets.FORGE_BOT_TOKEN }}` (already provisioned, broader scope).

### Failure resilience

- Silent-skip on any `gh api` failure now writes an explicit `::warning::` annotation, so missed alerts surface as visible build-output annotations instead of going unnoticed.
- Telegram send failure (network blip, bot revoked) logs a warning and exits 0 — failure-to-notify shouldn't compound into a second alert source.

Source: `openspec/archive/2026-05-25-add-deploy-failure-notifications/`. Pull-style companion (always-visible workspace shell pill): `openspec/archive/2026-05-25-add-deploy-state-header-pill/` (shipped same day in PR #1746; see `openspec/current/studio-workspace/spec.md § Deploy-state pill`).
