NOTQIN Workbench — Web App Spec v1

One sentence. A team-facing web app that exposes the NOTQIN vault as a filtered knowledge base + AI assistant + lightweight editor that commits back via PR.

Relationship to the existing Quartz portal. The portal is read-only static and excellent for casual browsing on phones. The Workbench is interactive, AI-augmented, and write-capable. Same audience model, same vault, different surface. Both stay live.

Status. Spec v1 locked from 4 design decisions (2026-05-24). Phase 0 scaffolding in progress. Build target: 3 weeks.


0. Goals & non-goals

Goals (v1)

  1. Every teammate can authenticate via Cloudflare Access (no new auth code) and see only the notes their audience tag permits
  2. Every teammate can chat with Codex, with Codex grounded in their filtered view of the vault (RAG over the audience-filtered corpus)
  3. Every teammate can view a knowledge graph of their accessible notes (nodes = notes, edges = wikilinks)
  4. Every teammate can create + edit notes in-app
  5. Every edit auto-commits to a per-session git branch; “Push” creates a PR back to main for Ahmed’s review
  6. Ahmed sees PRs in his usual GitHub flow; merging is the canonical write to the vault

Non-goals (v1)

  • Real-time multi-user co-editing (no CRDTs / OT — single user per session per branch)
  • Mobile-optimized editing (browse + AI works on mobile; editing is desktop-class)
  • Plugin system / extensibility
  • Public publishing (Quartz portal is for that)
  • Replacement of Obsidian for Ahmed’s personal workflow (Ahmed keeps Obsidian + git locally)

1. Architecture

                            ┌─────────────────────────────────┐
                            │  Cloudflare Access (magic-link) │
                            │  workbench.ahmedbenahmed.com    │
                            └────────────┬────────────────────┘
                                         │ Cf-Access-Authenticated-User-Email header
                                         ▼
   Browser  ◄── React/Vite SPA ─────► FastAPI on droplet :8000
   (Cytoscape.js graph,                    │
    CodeMirror editor,                     │
    AI drawer)                             │
                                           │
            ┌──────────────────────────────┼──────────────────────────────┐
            ▼                              ▼                              ▼
    ┌────────────────────┐       ┌────────────────────┐       ┌──────────────────────┐
    │ /srv/vault/        │       │ codex exec --      │       │ /srv/vault-views/    │
    │ (git clone of      │       │  sandbox=read-only │       │ <audience>/          │
    │  digitpme-vault)   │       │  --cwd <user-view> │       │ (4 pre-built filtered│
    │                    │       │  "<prompt>"        │       │  copies, rebuilt on  │
    │ branches:          │       │                    │       │  vault pull)         │
    │  main              │       └────────────────────┘       └──────────────────────┘
    │  <user>-<ts>       │                │                              │
    └─────────┬──────────┘                │                              │
              │                           │                              │
              ▼                           ▼                              ▼
      ┌──────────────────┐        Streams response          Audience filter
      │  origin GitHub   │        back to browser           reuses existing
      │  PRs → main      │                                  audience-map.json
      └──────────────────┘                                  + sync-content.mjs

Component summary

LayerTechWhy
AuthCloudflare Access (magic-link, already in use)Zero new auth code. Same allow-list config as portals. Header-passed identity.
BackendFastAPI (Python 3.12)Familiar, async-native, great for shelling out + streaming
Git layerGitPython OR direct git subprocessDirect subprocess is simpler + transparent. GitPython if testing matters more.
AI layerShell out to codex exec --sandbox read-only --cwd <view>Uses existing droplet Codex CLI auth (ChatGPT subscription). No OPENAI_API_KEY needed. Per codex skill conventions.
Audience filterReuse portal/scripts/sync-content.mjs (extract pure-function core)Same predicate as portal — single source of truth
FrontendReact + Vite + Tailwind + shadcn/uiAhmed’s preferred stack
EditorCodeMirror 6 + markdown modeIndustry standard, lightweight, no WYSIWYG complexity in v1
GraphCytoscape.jsMature, handles large graphs, good wikilink-style data fit
PR creationgh pr create --base main --head <branch> via subprocessUses droplet’s gh CLI with Ahmed’s PAT
Reverse proxynginxTLS termination (via CF), static SPA serving, FastAPI proxy
Process mgrsystemd unit for FastAPISimple, native to Ubuntu

2. Identity & audience model

How a user is identified

  1. Browser hits workbench.ahmedbenahmed.com
  2. Cloudflare Access intercepts, sends magic-link to their email
  3. After auth, every request to the origin includes header Cf-Access-Authenticated-User-Email: <email>
  4. FastAPI middleware reads the header, looks up the user in a YAML file:
# /srv/notqin-workbench/config/users.yaml
users:
  ahmedtanaltbenahmed@gmail.com:
    name: Ahmed Ben Ahmed
    audiences: [academic, ai, ot, gtm]   # founder sees all
    github_handle: Ahmed-BenAhmed
  elwalid@<email>:
    name: Elwalid
    audiences: [ai]
    github_handle: <tbc>
  imrane@<email>:
    name: Imrane
    audiences: [gtm]
    github_handle: <tbc>
  oussama@<email>:
    name: Oussama
    audiences: [ot]
    github_handle: <tbc>
  mouad@<email>:
    name: Mouad
    audiences: [ot]
    github_handle: <tbc>

users.yaml is the source of truth. Ahmed edits it directly when team membership changes; reloaded on every request (it’s small).

Where the filter is applied (defense in depth)

LayerFilter mechanismDefense against
Note tree (GET /notes)Walk filesystem, parse frontmatter, only return files where audience: intersects user audiencesWrong file listed in UI
Note read (GET /notes/{path})Re-check audience on the specific fileURL guessing
Codex scopecwd = pre-built filtered view directoryLLM citing files the user shouldn’t see
Graph (GET /graph)Same filter on nodes + edges (drop edges to filtered-out nodes)Knowledge-graph inference attacks
Note write (PUT /notes/{path})If new file: enforce default audience [ai, ot, gtm]. If existing: check user has write access to current audience set.Audience-tag escalation

3. Endpoint spec

Auth + identity

MethodPathAuthReturns
GET/api/meCF Access{email, name, audiences, github_handle, current_session?}

Notes (read)

MethodPathReturns
GET/api/notes/treeHierarchical tree of accessible notes (folders + files), grouped by top-level folder
GET/api/notes?path=<urlencoded>{path, frontmatter, body, audience, last_modified}
GET/api/search?q=<q>Full-text search within accessible notes

Notes (write — requires active session)

MethodPathBodyBehavior
POST/api/session/start{}Creates branch <user-slug>-<timestamp> off latest main, returns {branch, started_at}
PUT/api/notes?path=<urlencoded>{frontmatter, body, commit_message?}Writes file, auto-commits to session branch. If commit_message omitted, default = "edit: <path>"
POST/api/notes{path, frontmatter, body}Creates new note. If frontmatter omits audience, defaults to [ai, ot, gtm]
POST/api/session/push{pr_title?, pr_body?}Pushes branch, creates PR via gh pr create, returns {pr_url}
DELETE/api/session{}Abandon session (deletes branch locally, no push)

AI

MethodPathBodyReturns
POST/api/ai/chat{prompt, conversation_id?}Server-sent events stream of Codex output
GET/api/ai/conversationsList of user’s recent conversations
GET/api/ai/conversations/{id}Full conversation history

Graph

MethodPathReturns
GET/api/graph{nodes: [{id, label, type, audience}], edges: [{source, target, kind}]} — filtered
GET/api/graph?focus=<path>&depth=<n>Subgraph around a focal note

4. Codex integration details

Why Codex CLI (not OpenAI API directly)

  • Uses Ahmed’s existing ChatGPT subscription auth on the droplet — no separate OPENAI_API_KEY to manage
  • Per codex skill conventions in ~/.claude/skills/codex/
  • --sandbox read-only enforces filesystem read-only at the OS level — even if the prompt asks Codex to “write to <sensitive-file>”, the sandbox refuses

The invocation pattern

codex exec \
  --sandbox read-only \
  --cwd /srv/vault-views/<audience>/ \
  --skip-git-repo-check \
  --output-format=plain \
  "<user prompt with system framing>"

System framing prefix (prepended to every prompt)

You are NOTQIN's AI assistant, helping {user_name} (audience: {audiences})
work with their accessible vault. The current working directory contains
ONLY the notes this user can see. Cite source files when you make claims.
Today's date is YYYY-MM-DD.

User question:
{user_prompt}

Conversation memory

Each conversation gets a uuid. Backend persists conversations/<uuid>.jsonl with appended turns. Codex doesn’t have built-in session memory across exec calls — we re-inject the last N turns as context on each invocation.

Streaming

codex exec writes to stdout. FastAPI StreamingResponse pipes it to the browser as SSE.

Cost

  • Codex CLI auth = ChatGPT subscription — fixed cost, no per-token billing
  • For a 5-person team with moderate use, no incremental cost
  • If usage spikes: switch to OpenAI API with rate-limiting per user

5. Per-audience filtered views (storage)

The vault is git cloned once at /srv/vault/. Filtered views live alongside:

/srv/
├── vault/                    ← live git repo
│   ├── main (default checkout)
│   └── .git/
├── vault-views/              ← rebuilt on every git pull
│   ├── academic/
│   ├── ai/
│   ├── ot/
│   └── gtm/
└── notqin-workbench/         ← the FastAPI app
    ├── config/users.yaml
    ├── app/
    └── conversations/

Rebuild trigger

  1. FastAPI startup → pulls latest, rebuilds all 4 views
  2. After any PR merges to main (GitHub webhook → POST /api/internal/refresh → pull + rebuild)
  3. On a 15-min cron as fallback

Rebuild mechanism

Extract the pure filtering core from portal/scripts/sync-content.mjs into a reusable function. Call it from a small Python wrapper (vault_views.rebuild()) that invokes node with the per-audience target.


6. Branch & PR flow

Session start

  • User clicks “Start writing” → POST /api/session/start
  • Backend: git checkout main && git pull origin main && git checkout -b elwalid-2026-05-26-1430
  • Branch name pattern: <user-slug>-<YYYY-MM-DD>-<HHMM> — unique per session, human-readable
  • All subsequent writes commit to this branch

Per-edit commits

  • Every PUT /api/notes and POST /api/notes auto-commits with message "<user>: <commit_message or default>"
  • Author = the user’s GitHub handle from users.yaml, falling back to Ahmed’s account if not set
  • Commits are squash-friendly (PR review can squash on merge)

Push & PR

  • User clicks “Push for review” → POST /api/session/push
  • Backend: git push origin <branch> then gh pr create --base main --head <branch> --title "<pr_title or auto>" --body "<pr_body or auto>"
  • Default PR body: lists all commits + diff summary + the user’s name
  • Returns pr_url → frontend shows “PR created →

Ahmed’s review side

  • Standard GitHub PR review (using existing flow)
  • Merge → CF Pages portal auto-rebuilds (existing pipeline) AND the workbench gets a webhook → pulls fresh
  • Workbench’s session-branch handling is at-most-once: after PR merge, the user’s session ends; new session starts fresh from updated main

Conflict handling

  • Push fails on conflict (branch is stale vs current main)
  • Frontend shows: “Main has new commits since you started. Discard session and start fresh? [Discard + restart] [Cancel]”
  • v1 does NOT attempt automatic rebase / merge resolution — too risky for non-engineers

7. Editor UX (CodeMirror 6)

  • Markdown source mode (no WYSIWYG in v1 — keeps git diffs clean)
  • Frontmatter shown in a structured panel at the top (audience picker, tags, type, owner)
  • Body in CodeMirror with: syntax highlighting · wikilink autocomplete (from accessible note paths) · live preview pane (toggleable)
  • Save button or auto-save every 5s while focused → debounced PUT
  • Visual indicator: ”● Unsaved” / ”✓ Saved to

8. Knowledge graph UX (Cytoscape.js)

  • Default view: all accessible notes as nodes, wikilinks as edges
  • Node color by type: (concept-reference, strategy, person, etc.)
  • Node size by in-degree (most-linked-to = biggest)
  • Click node → open in editor
  • Filter sidebar: by type:, by folder, by tag
  • Search box → highlight matching node + neighbors

9. AI drawer UX

  • Right-side slide-in drawer (toggleable with Ctrl/Cmd+K or button)
  • Conversation list at top
  • New message → streamed Codex response with file citations
  • Click any citation → opens that note in the main pane
  • “Insert as new note” button — turns the AI response into a draft note in the editor (with default [ai, ot, gtm] audience)
  • “Quote in current note” — inserts citation block at cursor in active editor

10. Infrastructure & deploy

The droplet

  • Existing DO box with Codex installed (per user)
  • Ubuntu 22.04 / 24.04
  • Add: Python 3.12, nginx, node 20+, gh CLI authenticated to Ahmed’s GitHub, gpg for signed commits (optional)
  • Disk: ~5 GB for vault + views + conversations + logs (negligible)
  • RAM: 1 GB sufficient for FastAPI; Codex spawns its own process per chat

DNS

  • workbench.ahmedbenahmed.com CNAME → droplet IP (via Porkbun, like factories)
  • Cloudflare Access fronts it via custom hostname

CF Access app

  • New Access application named “NOTQIN Workbench”
  • Domain workbench.ahmedbenahmed.com
  • Allow-list emails (Ahmed + 4 teammates initially)

TLS

  • nginx with Let’s Encrypt (certbot) — standard, free

Systemd unit

# /etc/systemd/system/notqin-workbench.service
[Unit]
Description=NOTQIN Workbench FastAPI
After=network.target
 
[Service]
Type=simple
User=workbench
WorkingDirectory=/srv/notqin-workbench
ExecStart=/srv/notqin-workbench/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=on-failure
 
[Install]
WantedBy=multi-user.target

Repos

RepoWhere it lives
Ahmed-BenAhmed/digitpme-vaultThe vault. Stays vault-only.
Ahmed-BenAhmed/notqin-workbenchNEW. The workbench app (backend + frontend).

11. Phased build plan

Phase 0 — scoping & scaffold (this week, 1–2 hours)

  • Spec written (this doc)
  • Create Ahmed-BenAhmed/notqin-workbench repo (private)
  • Scaffold repo structure (backend + frontend dirs + README + .gitignore)
  • Confirm Codex CLI working on droplet (ssh root@monmachine; codex --version)
  • Verify droplet has Python 3.12 + node 20+ + nginx + gh CLI (install missing)
  • Create users.yaml template
  • Set up CF Access app for workbench.ahmedbenahmed.com (just the policy — domain comes later)

Phase 1 — backend skeleton (week 1, ~4 days)

  • FastAPI app boilerplate + CF Access middleware
  • GET /api/me returning identity + audiences
  • GET /api/notes/tree with audience filter
  • GET /api/notes?path= reading individual files
  • Per-audience view builder (Python wrapper around sync-content.mjs)
  • POST /api/ai/chat shelling out to codex exec with streaming SSE
  • Conversation persistence to conversations/<uuid>.jsonl
  • GET /api/graph — parse wikilinks + frontmatter related:, build node/edge JSON
  • systemd unit + nginx config + Let’s Encrypt
  • Smoke test via curl from local machine

Phase 2 — frontend skeleton (week 2, ~4 days)

  • Vite + React + Tailwind + shadcn boilerplate
  • Auth-aware layout (read CF Access header on first /api/me call)
  • Note tree component (left sidebar)
  • Note viewer (markdown rendered via remark/rehype)
  • AI drawer (right slide-in, streaming chat with citations)
  • Graph view (Cytoscape.js, lazy-loaded)
  • Search bar (FastAPI-side full-text or client-side fuse.js for small corpus)

Phase 3 — edit + commit + PR flow (week 3, ~4 days)

  • CodeMirror 6 editor integration
  • Frontmatter form (audience picker, tags, etc.)
  • POST /api/session/start + UI
  • PUT /api/notes autosave + commit
  • POST /api/session/push + gh pr create + UI showing PR URL
  • Conflict-handling UI (“main has new commits, restart session?“)
  • New-note creation flow with audience default

Phase 4 — deploy + team onboard (end of week 3)

  • Deploy backend to droplet via systemd
  • Build frontend, serve via nginx
  • CF Access app activated, custom domain pointed
  • Add 4 teammate emails to allow-list
  • Walk each teammate through first login + first edit + first PR
  • Update Team Access Guide with workbench URL

12. Risks & open questions

Risks

RiskMitigation
Codex CLI auth on droplet expires / breaksDocument re-auth flow; alert on /api/ai/chat failures; fallback to OPENAI_API_KEY if catastrophic
Audience filter bug leaks a founder-only docTriple defense (tree + read + AI cwd); test suite covering each layer; security audit before opening to team
Concurrent writes corrupt the vaultSingle user per branch = no concurrent writes within a branch. Cross-branch conflicts surface at push, handled.
Droplet goes downStandard DO uptime; consider snapshot backups weekly
gh pr create rate limits or token expiresStandard GitHub limits are generous; monitor; rotate token annually
Editor session abandoned mid-writeAuto-save covers most cases; orphan branches cleaned up by weekly cron (git branch -D <name> if older than 14 days and not pushed)
LLM hallucinates an audience: value or pathSystem prompt explicitly forbids; even if it does, the next save’s audience filter will reject

Open questions for Phase 0

  • GitHub author of commits — Ahmed’s account (simpler, all PRs look like Ahmed’s) or per-user via git commit --author? Per-user is honest, requires real email per teammate.
  • Should the workbench show a “live audience preview” — toggle between “view as me” and “view as “? (Founder-only feature, useful for sanity-checking.)
  • WebSocket vs SSE for chat streaming. SSE simpler, works fine for one-direction streaming. Lean SSE for v1.
  • How to handle binary attachments (images embedded in notes) in v1? Skip → v2 problem.
  • Should PRs auto-tag Ahmed for review? Yes — gh pr create --reviewer Ahmed-BenAhmed.

13. Success criteria (how we know v1 works)

  1. All 4 teammates log in and see only their audience-filtered notes ✅
  2. Each teammate has a successful Codex conversation grounded in their corpus ✅
  3. Each teammate creates one new note + edits one existing note + opens a PR ✅
  4. Ahmed reviews + merges at least 4 PRs (one per teammate) ✅
  5. Knowledge graph renders for each audience without performance issues (<100 ms for <500 nodes) ✅
  6. Zero audience-leak incidents reported in first 2 weeks of use ✅

See also

  • Team Access Guide — current portal access (read-only)
  • Portal Audience Routing 2026 — the audience-tag system reused here
  • Augmented Intelligence and RAG — the theoretical pattern
  • NOTQIN Agent — the industrial RAG application; Workbench is the internal RAG application
  • KMS Classification — Workbench advances NOTQIN from Type-1 (Repository) to Type-1+4 (Application)
  • Stakeholder Portal v1 — sibling read-only static portal (stays live)
  • ~/.claude/skills/codex/ — Codex CLI delegation conventions