teamwork-vault · architecture & roadmap

Current state, and what’s next

One Cloudflare Worker, live and serving today. Here’s exactly how it’s built, where the seams are, and the four scoped enhancements that take it forward — with seven decisions to capture.

LIVE · nobox.teamwork.chem.dev · noboxAI tenant · June 2026

What it is today

A business-case-agnostic, multi-tenant, multi-user team workspace exposed as a remote MCP server. One Worker hosts unlimited tenants at $0 incremental each — a tenant is a row, not a deploy.

Every D1 query filters by tenant_id. Tenant isolation is structural, not a check you can forget. — the hard rule the whole design is built around

Worker

The dispatch spine

Host→tenant resolve, HMAC bearer identity, then one gate every tool passes: allowed→feature→role→zod. Audit row on every path.

D1

The index

Artifacts row, FTS5 search, links, tasks, audit, oauth. The source of truth for visibility.

R2

The durable bodies

.md bodies keyed <tenant>/<kind>/<id>.md. Last-writer-wins, no git friction.

KV

The fast path

Tenant cache (60s), oauth clients, one-time invite/auth codes.

A request, end to end (save flow)

sequenceDiagram
    autonumber
    participant C as MCP Client
    participant W as Worker
    participant K as KV
    participant D as D1
    participant R as R2
    C->>W: POST /mcp (Bearer)
    W->>K: resolve tenant by host
    K-->>W: tenant (60s cache, D1 fallback)
    W->>W: verify bearer · tenant_id == host
    W->>W: dispatch gate (allowed, feature, role, zod)
    W->>R: put body
    W->>D: upsert index + FTS
    W->>D: audit_log (every path)
    W-->>C: result
      

Where the seams are

Three gaps the design names itself — none invented here. They’re the openings the roadmap fills.

  • The orphan sweep is named but not built. R2 writes before D1, so a failed index leaves a harmless orphan — “sweep later.” Later hasn’t shipped.
  • No revision history. R2 has no native versioning; a save overwrites in place, last-writer-wins.
  • Search is keyword-only. FTS5 finds the words you typed, not the ones that mean the same thing.
  • Media is a pointer. We store the Loom/Zoom link, not the media. Fine today — a known seam tomorrow.

Where the effort lands across the four enhancements

pie showData title Relative build effort
    "R2 sweep + lifecycle (now)" : 15
    "App versioning (phased)" : 20
    "Vectorize semantic (deliberate)" : 40
    "Images attachments (when needed)" : 15
    "Stream (deferred)" : 10
      

The future state

Additive, all-Cloudflare, isolation-preserving. The save path gains an async embed; search becomes hybrid; a nightly job keeps R2 and D1 honest.

Save & search, after the enhancements

sequenceDiagram
    autonumber
    participant W as Worker
    participant AI as Workers AI
    participant V as Vectorize
    participant D as D1
    participant R as R2
    W->>R: put body (+ customMetadata)
    W->>D: upsert index + FTS
    W->>AI: embed chunks (async)
    AI-->>W: vectors
    W->>V: upsert (tenant_id metadata)
    Note over W,V: search = FTS ∪ Vectorize → D1 visibility gate
      

The gate never moves. A vector hit is untrusted recall — every result is re-checked against D1’s tenant + visibility rules before it’s returned. Vectorize is search-space, never the security boundary.

The four enhancements

Each is fully scoped in docs/superpowers/ — a deep dive, three design specs, and one executable plan.

Greenlight first · ~$0

1 · R2 versioning & orphan-sweeper

customMetadata stamping + two lifecycle rules + a daily Cron list-and-diff sweeper that reconciles R2 against D1 with a grace window. App-level versioned keys give opt-in history (R2 has none natively). Closes a named gap, no new product surface.

The real upgrade

2 · Vectorize semantic search

Hybrid keyword + meaning, beside FTS5. Load-bearing detail: a tenant_id metadata index created before any insert, every query filtered by it, every hit re-verified in D1. Defer until keyword search demonstrably misses — then the plan is ready.

When needed

3 · Cloudflare Images

Transform images straight off the existing R2 bucket — no migration, Free tier (5k transforms/mo). The lightest-touch path to artifact image attachments, reusing the same key convention and tenant invariant.

Hold

4 · Cloudflare Stream

Hosting recordings, not linking them. Deferred: URL-ingest needs a direct MP4, and Loom/Zoom share links are HTML pages. The pointer model is correct until those links start rotting.

How it sequences

Lowest-risk, highest-leverage first. Nothing here blocks the live service; every step is additive and reversible.

A recommended shape — your call on each gate

First commit
R2 customMetadata + lifecycle + Cron sweeper
Next
App-level versioned keys + history read
When it misses
Vectorize hybrid search (isolation-first)
On demand
Images attachments · Stream on a hard trigger

Seven decisions decide what ships first. Each one already has a recommended option — confirm or redirect whenever you have a minute.

Capture the decisions →