TARGET_ARCHITECTURE.md — Confluence-Class Target State (Phase 0)
Target architecture after the Confluence expansion. Principle: additive extension of the existing modular monolith — no new framework, database, engine, or service. Every new capability is an extension of an existing module, package, or table (per
ARCHITECTURE.mdinvariants + spec §18).
1. What does NOT change
- Four-service topology (web / api / worker / realtime) — unchanged.
- Layering: Editor → Domain → Service → Repository → PostgreSQL — unchanged.
- Prisma + PostgreSQL single schema; migrations remain the only schema path.
- Fractional ordering (LexoRank) for all trees/lists — unchanged.
- Domain events on Redis streams for side effects — unchanged (new event types join the same bus).
packages/permissionsremains the single authorization engine; no second permission system (spec §2.6).- OpenSearch remains the search engine (scale already proven; no new engine).
- BullMQ remains the job system (no new queue).
- Yjs remains the CRDT (whiteboard realtime deferred, decision A-004).
- AI gateway/provider abstraction — new capabilities are new tools on it.
2. New domain surfaces (by module)
2.1 workspaces module (Spaces)
Workspace.type(string enum:team|project|kb|custom, defaultteam)archivedAt.
- New endpoints:
GET /workspaces(directory: member-scoped list with type/search/sort),GET /workspaces/:id/stats(counts + storage),POST /workspaces/:id/archive//restore, cross-workspace page move (POST /pages/:id/move {toWorkspaceId}with dual permission checks). - No new tables beyond columns.
2.2 pages module (hierarchy + lifecycle + backlinks + versions)
POST /pages/:id/duplicate— recursive copy (new ids, positions re-based, block tree + database refs copied lazily: databases copied by snapshot).POST /pages/bulk—{action: trash|archive|favorite|unfavorite, ids[]}→{succeeded[], failed[{id, code}]}; per-item permission check; audit.- Backlinks:
GET /pages/:id/backlinks— maintained by an in-DB scan over referencing block types (child_page,synced_block,linked_database) and commentpageMentionIds; result cached (version-key bust on reference changes via existing events). No new search engine. Ponytail note: a dedicatedbacklinkstable is not justified at current scale — the scan is O(references-in-workspace) and covered by an existing index; if p95 > 100 ms under load test, promote to a maintained table (recorded upgrade trigger). - Review workflow (Domain J):
Page.reviewState(string, defaultpublishedfor legacy pages — backward compatible),Reviewmodel (pageId, state, reviewerIds, requestedBy, comments, deadline, decision, decidedAt, decidedBy). EndpointPOST /pages/:id/review {action: submit|approve|request_changes|withdraw, ...}with server-side transition validation +REVIEW_REQUESTED/APPROVAL_DECISIONnotifications + audit. - Version compare:
GET /pages/:id/versions/compare?from&to— block-tree diff (added/removed/changed, by block id + content hash; LCS over id sequences) computed inpackages/shared(pure, unit-tested); restore always appends a newPageVersion(existing behavior verified + tested).
2.3 templates (in pages module — kept there; Ponytail: no new module for 6 endpoints)
Templatecolumns:category,description,variables Json,defaultFor String?(new-page default). NewTemplateVersiontable.POST /templates/:id/instantiate— accepts variable values; substitution engine in service (Zod-validated variable schema); produces normal blocks.- 15 built-in templates: idempotent seed section in
prisma/seed.ts(isBuiltin: true, workspace-scoped to the personal/template workspace or org-level asworkspaceId = null-equivalent decision — decision A-005: built-ins seeded per-workspace on workspace creation, so the existing workspace-scoping + permission model is untouched).
2.4 comments module (anchor hardening)
- Selection anchor:
{startBlockId, endBlockId, startOffset, endOffset, prefix, suffix}(first/last 24 chars of anchored text). On render, anchor resolved by block + prefix/suffix search; on failed resolution the comment stays attached tostartBlockIdwith an "original selection" badge and the stored text. Deleted block → comment survives on the page (page-level fallback) — never orphaned (spec §7).
2.5 search module (discovery)
UserRecentItem(userId, itemType, refId, at; capped 50/user, FIFO) +SavedSearch(name, queryJson). Endpoints:GET /me/recent,POST/DELETE /me/saved-searches, related-pages section reuses backlinks. Admin reindex:POST /admin/search/reindex→ enqueues full-scan worker job (idempotent).- Index metadata gains
labels+lifecyclefor filtering.
2.6 databases module
POST /databases/:id/import/csv— multipart CSV, header → property mapping (dry-run first: returns mapping + row-error preview), then transactional insert; reuses record-creation service (no second writer).- Saved-view management endpoints (rename, set default, visibility) on
existing
DatabaseView.
2.7 New whiteboards module (Domain I)
Whiteboardmodel: pageId 1:1 (a whiteboard page = aPagewithkind='whiteboard'— decision A-001: whiteboard is a page kind, reusing page tree, permissions, comments, versions, search — no parallel hierarchy),canvas Json(versioned),schemaVersion Int @default(1),updatedBy.packages/whiteboard(new small package): canvas JSON schema + pure geometry/undo helpers. Rendering: hand-rolled SVG renderer inapps/web(decision A-001) — no new dependency (spec §12 asks to evaluate OSS libs; evaluation conclusion: a sticky-notes/shapes/connectors/freehand canvas is ~1.5k LoC of SVG + pointer math, and a foreign canvas library would drag in a React-version or license risk with no feature the spec requires that we cannot own; revisit if multi-user whiteboard realtime is adopted).- Undo/redo: in-memory op stack (editor-style history pattern from
packages/editor) + canvas persisted as a PageVersion snapshot on save. - Objects: sticky note, text, rect/ellipse/diamond, connector (orthogonal +
straight, with arrows), freehand path, group. Links: sticky/text can carry
pageReforurl; "convert selection → page" builds real blocks.
2.8 New tasks module (Domain J)
Taskmodel: workspaceId, pageId?, title, description?, assigneeIds[], createdBy, status (todo|in_progress|done|cancelled), dueAt?, checklist (a task is aPagewithkind='task'+Tasksidecar — decision A-006: page-backed so blocks/checklists, comments, permissions, search all work for free; the sidecar table holds queryable columns assignee/status/due).GET /tasks?scope=inbox|all&status&due(personal inbox = assignee=me), CRUD,POST /tasks/:id/done. Reminder: BullMQ delayed job per dueAt →TASK_ASSIGNED/reminder notification (existing channel).taskblock type (renders card, links to task page).
2.9 notifications / activity
- New
NotificationTypevalues:REVIEW_REQUESTED,APPROVAL_DECISION,WATCH_UPDATE,TASK_DUE. PageWatch(userId × pageId) + watch endpoints; feed endpoints:GET /me/activity,GET /workspaces/:id/activity— served fromAuditLog/domain-event projections (no new event store — Ponytail: the audit log already records actor/action/resource/time; the feed is a permission-scoped query + projection, not a new pipeline).
2.10 ai module (assistant capabilities)
- New tools on the existing gateway, all permission-filtered through
packages/permissionsbefore retrieval (invariant L-1), all cited, all logged:extract-actions(→ tasks),generate-faqs(→ draft page),find-duplicates(embedding similarity over search index, no new vector store),find-stale(updated-at heuristic + LLM contradiction check),suggest-metadata(owner/tags/review date → apply via normal APIs). - Reuses RAG service chunker + embeddings (
ai_embeddings); no new infra.
2.11 Admin (org console)
- New tabs: Jobs (BullMQ queue depth/failures via in-process queue inspection, read-only), Search health (doc count, last reconcile, lag, reindex button), Notifications health (send/failure counts, recent failures). Read-only surfaces over existing stores — no new tables except nothing (queue state is transient by design).
3. Event catalog additions (single source of truth, packages/shared)
page.duplicated, page.moved.workspace, page.bulk, review.submitted,
review.approved, review.changes_requested, review.withdrawn,
task.created, task.assigned, task.status_changed, task.due,
whiteboard.created, whiteboard.updated, watch.added, watch.removed,
template.instantiated, template.versioned, search.reindex_requested.
All ride the existing stream; consumers: search indexer (new types indexed), notification fan-out, webhooks (catalog entries added).
4. Frontend additions
| Route / surface | Feature |
|---|---|
/spaces |
Directory (filter type/search, sort) + stats chips |
/inbox |
Task inbox (mine/due/overdue/done) + task calendar tab |
…/page/[id] (whiteboard kind) |
Whiteboard editor (full-screen canvas mode) |
…/page/[id]/history panel |
Version list + compare/diff view |
…/page/[id] review banner |
State chip + submit/approve/request-changes |
…/page/[id] related panel |
Backlinks + related pages + watch toggle |
/templates |
Search, categories, preview, instantiate-with-variables dialog |
/search + ⌘K |
Recently viewed tab, saved searches, related section, label filter |
| org admin | Jobs / Search health / Notifications health tabs |
All reuse packages/ui, TanStack Query, Zustand, React Hook Form — no new
UI framework (spec §19).
5. Deployment impact
- Migrations: one additive migration set (details in DATABASE_IMPACT.md). No backfill except built-in template seeding (idempotent, per-workspace).
- Env vars: none new (AI uses existing keys; whiteboard uses no external service). No new docker services.
- Workers: two new job types (task-due reminders, search full-reindex); both on the existing BullMQ topology.
- OpenSearch: mapping update (metadata fields labels/lifecycle) — reindex via the new admin tool after deploy.
- Rollback: all columns additive/nullable with defaults; dropping is the rollback path (documented).
6. Explicit non-goals (this expansion)
- Custom roles / policy-expression permissions (deferred; documented in DECISIONS A-003).
- External third-party connectors (A-002) — integration surface + docs only.
- Realtime (multi-user) whiteboard editing (A-004) — single-user first; CRDT extension is a follow-up task.
- Group (team) mentions — user mentions only this round (recorded limitation).
- Whiteboard object-level comments — whiteboard-level comments only.