Skip to content

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 changes row 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

  1. Folder module (now): convex/<module>/ with an index-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.
  2. 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.
  3. 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: page through 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.