Module Architecture (Proposal)¶
Status: draft. Draft ADR-040. Builds on Work Model & IA (object types, the one write path) and Repo Ingestion (the first connector). Applies from the wiki onward; existing code moves into it as it is touched.
1. Principle¶
Thinklio is a product; Novansa is its first account. Everything built for the pilot must work for any account. That means:
- Account facts live in data, not in code or environment variables: connections, mappings, defaults and settings belong to an account (or a person) and are changed in the app.
- One contract per kind of module. A new object type or connector plugs into the same change log, access rules, search, history, API and agent tools without special cases elsewhere.
- Formats are published and versioned where outsiders depend on them (the ingest spec is the first).
2. Four kinds of module¶
| Kind | What it is | Examples | Contract |
|---|---|---|---|
| Object type | Something work is made of, stored in Thinklio | tasks, list records, pages; later files, threads, contacts | §3 |
| Connector | An outside system content comes from or goes to | GitHub, Notion, Google Workspace, Cliniko, Xero, CSV | §4 |
| Capability | Something people or agents can do, through the app, the API, MCP and agent tools | create a task, update a record, search, propose an effect | §5 |
| Surface | How an object type looks and is edited in a client | web and Flutter renderers per type and view | §6 |
A module is a folder that provides any of these, with a small public surface. Pages provides an object type, its capabilities and its surfaces; GitHub provides a connector; Notion provides a connector used in import mode.
3. Object types¶
Every object type implements one contract, registered in convex/objects/registry.ts:
- Tables and identity: its table, an account, usually a space.
- Write path: create, update, archive or delete, restore, move; every write records a
changesrow in the same transaction (actor, source, basis, field-level diff). Tasks (lib/taskWrites) and records (lib/recordWrites) already do this. - Undo: reverse a change through the write path, refusing when the object has moved on.
- Access: reads and writes go through space access (
lib/spaces). - Describe: a title for activity and search, field labels for history.
- Read-only marker: an object mirrored from a connector is read-only, and undo is off for its source changes.
- Search and links: a search index and participation in links and backlinks.
The registry replaces today's per-type switches (undo in changes.ts, titles in activity, history wording in the web app) with one lookup, so What changed, history, search, undo, the API and agent tools work for every type, including future ones.
4. Connectors¶
4.1 Modes¶
| Mode | Owner after | Example | Behaviour |
|---|---|---|---|
| Mirror | the source | GitHub repo lists and pages | Continuous; read-only in Thinklio; history credited to the source's commits |
| Import | Thinklio | Notion migration, CSV | Once (or until cutover); objects become ordinary Thinklio objects; provenance kept |
| Link | the source | Cliniko patient, Xero bill | A live external field with actions; nothing copied except what policy allows |
4.2 The bundle¶
Connectors don't write objects. They produce a bundle, the intermediate form every connector shares, and one applier writes it through the object write paths:
documents: [{ ref, title, markdown, parentRef?, files: [fileRef] }]
tables: [{ ref, title, columns: [{ name, type, choices? }], rows: [{ ref, cells, body?, files? }] }]
files: [{ ref, name, contentType, size, storage }] // copied into the account's bucket first
The applier maps bundle items to object types (documents → pages, tables → lists, rows → records, files → attachments), by account decision with sensible defaults. It is idempotent by ref, and records provenance (source: { kind: "github" | "notion" | …, ref }). Mirror mode reconciles (create, update, archive); import mode creates once and skips what it has seen.
The GitHub connector's Markdown parser already produces a table; the Notion connector produces documents, tables and files. A CSV import is a table. Google Docs, Confluence or Drive folders would be more connectors producing the same bundle, with nothing downstream changing.
4.3 Connections¶
A connection belongs to an account (shared sources: a repo, a team Notion) or to a person (their mailbox, their Cliniko key). Credentials attach to principals (ADR-025) and are never stored in object data. Each connector declares how it authenticates (OAuth, app installation, per-user key) and what it can read and do.
5. Capabilities¶
Capabilities are generated from object type and connector definitions rather than hand-written per surface:
- App: the web and Flutter clients call the type's queries and mutations.
- Platform API and MCP:
/v1/{type}routes and MCP tools (pilot plan §5.6) from the same validators. - Agent tools: the same operations, with the agent as actor, through the write path and the proposal rules (work-model proposal §6).
One validator per operation feeds all four, so a field added to pages appears in the app, the API, MCP and agents together.
6. Surfaces¶
Field types, view types and object types are defined once in Convex; each client has a renderer per type (work-model proposal §12). On the web, renderers live in components/objects/<type>; Flutter mirrors them. A new object type ships with its web renderer; Flutter follows.
7. Packaging¶
- Folder module (now):
convex/<module>/with anindex-style public surface; other code imports only that. New modules start here (pages, the Notion connector); existing code (tasks, lists, ingest) moves when next touched. - Convex component (when shared across apps): own tables, an app-facing API, callbacks into the host's write paths. Candidates: ingest (Ripplebase also ingests repos), pages and the editor (Ripplebase edits content), files.
- Per-account enablement (with pricing): an account turns modules on (pages, a connector) and sets their settings; the agent catalogue and plan limits read the same record.
8. First applications¶
- Pages (the wiki) is built as the first module under this contract, with the registry.
- Tasks and records register in the registry; their existing write paths stay.
- The Notion migration is the Notion connector in import mode: it reads Notion into a bundle (pages as Markdown, databases as tables, files copied into R2), and the applier writes pages and lists. Novansa's import is its first use; any account can use it later with its own Notion connection.
- Repo ingestion gains
ingest: pagethrough the same applier.
9. Draft decision¶
Draft ADR-040: Thinklio Is Built from Modules: Object Types, Connectors, Capabilities and Surfaces¶
Date: 2026-10-05 Status: Proposed
Context: The pilot added tasks, list records and repo ingestion in quick succession; each needed the change log, undo, access, history wording and activity, added by hand per type. The Notion migration and the wiki would add two more. Everything built for Novansa must work for any account.
Decision: Object types implement one contract and register in an object registry; connectors produce a shared bundle written by one applier through the object write paths, in mirror, import or link mode; capabilities for the app, API, MCP and agents come from the same definitions; clients render each type with per-type renderers. Modules start as folders with a public surface and become Convex components when another app needs them; accounts enable modules and own their connections and settings.
Reasoning: One contract makes new types and sources cheap and keeps history, undo, search and agent access consistent across them. A shared bundle separates reading a source from writing Thinklio, so importers and mirrors reuse the same, tested write paths.