Skip to content

CreatorTrack: Architecture, How It Works, and the Rebuild Plan

URL: https://mkdocs.justinsforge.com/memory/plans/workspace-suite-architecture-and-rebuild-plan-2026-06-17/

CANON UPDATE (2026-06-18) , renamed to CreatorTrack. The product formerly called "The Suite" / "Workspace" is now CreatorTrack, built and previewed at dev.creatortrack.ai. Build order: reusable UI components first (one shared kit), then a Notion-class workspace/page framework as the foundation, then apps (finance, clips, CRM, etc.) that plug into that page framework as configs/modules; we iterate back-and-forth across kit, framework, and apps until each aspect's features fully work. The data layer formerly "lifeos" is now the core database (core); "workspace" survives only as the internal RLS data-tenant term. Full canon (with tech-stack overview): creator-suite-vision-strategy.

Author: [Claude Code], with Justin. Date: 2026-06-17. Status: vision + plan, decisions partly pending.

This is the canonical document for what Justin is building, how the pieces actually work, the stack decision and its rationale, and the phased plan to get there. It consolidates a long architecture conversation into one reference. Read this before building or advising on the workspace suite.


1. The vision

Justin is building a personal operating system: one self-hosted suite that runs his life and businesses, made of many tools that share one foundation.

  • Tools in scope: a Notion-style workspace (docs/pages), finances, fitness, calendar, habits, tasks, inbox, email, CRM, time tracker, plus a home dashboard.
  • Each tool should have a totally custom face (finances looks like a finance dashboard, calendar like Google Calendar, habits like a habit tracker), not the generic Notion database UI. The database/notes UI is just one face among many, not the building block.
  • The dream (not required now, but the design must not block it): build it for himself, then his brother Michael, then offer it as a business-manager product for other creators. That makes it a replicable, multi-tenant asset, not a pile of one-off apps.

The throughline: build the right foundation now, while almost nothing real is built, so the suite becomes an asset that scales from one user to many.


2. First principles (the mental model that makes it tractable)

  1. Separate the data from the presentation. The durable asset is the data model. The UI is replaceable and should be treated as disposable relative to the schema.
  2. Every tool is the same three things: a data model (what is a task / transaction / habit), operations on it (create, edit, link, roll up, filter), and a view that presents it. Notion, QuickBooks, a CRM, a calendar: all this shape.
  3. The suite is many custom views over one data spine. A new tool is not a new app; it is a new view plus maybe a new table. This is what makes it replicable and teachable.
  4. A page is a door, not the owner. Data lives at the workspace level in Postgres. A page, and a subdomain like finances.justinsforge.com, are entry points (views) onto the same data, never copies. This is why "the same finances are viewable in two places" works, and why the home dashboard can show a finance summary without duplicating anything.
  5. Multi-tenancy is the product path, and Row-Level Security is its primitive. Going from "me" to "me + Michael" to "creators" is multi-tenancy: many users/workspaces sharing one system, each seeing only their own data. Postgres RLS (already in place) enforces that at the database itself. The dream does not require building for the dream now; it requires not blocking it, and a clean data model + RLS does not block it.

3. What exists today (honest current state)

Surface What it is Runs as
Data layer PostgreSQL 17 on CT 109 (lifeos schema), JSONB + Row-Level Security, roles lifeos_app/lifeos_admin, Redis on same container The crown jewel. Stays.
workspace Flask app, Python building HTML from f-strings, vanilla JS embedded as raw strings forge-workspace.service (running)
finances Same Flask server-rendered pattern as workspace forge-finances.service (running)
fitness forge_fitness_app.py + forge_fitness_dashboard.html, fed by the Hevy pipeline; lighter, not a persistent running service on-demand / generated
justinsforge.com home A static site (sites/justinsforge.com): plain HTML/CSS/JS, no backend logic served as static files
Auth Cloudflare Access in front (header Cf-Access-Authenticated-User-Email mapped to the RLS user) already solved
Deploy LXC + systemd --user units on Console/Finn, behind Cloudflare tunnels; no build step works

Why it fights us: the workspace and finances are built as server-rendered HTML strings plus vanilla JavaScript embedded inside Python strings. Every micro-interaction (multi-select, popovers, drag, "click anything to get a dropdown") is hand-wired; state is scattered across the DOM; JS-in-Python-strings has no type or syntax checking and produced repeated escaping bugs. This is a Notion-class product built on a Wikipedia-class foundation, which is the root of the friction.


4. How web apps actually work (the mechanics)

Three actors, and the key is knowing where each runs:

  1. Browser (the client) runs on Justin's device (iPhone, Windows). Its only job: turn HTML/CSS/JS into what you see, and react to taps/clicks.
  2. Server (the back-end) runs on Console/Finn, not the phone. It runs code, makes decisions, talks to the database.
  3. Database (Postgres on CT 109) stores everything and answers queries.
  4. Cloudflare sits in the middle as a doorway: proxies traffic, checks identity, tunnels to Finn.

The front-end is three languages, one job each: - HTML = structure (the skeleton). Nouns. - CSS = appearance (color, layout, the pill on a tab). Adjectives. - JavaScript = behavior (clicks, fetching, changing the page live). Verbs. - The browser is the engine that reads all three and paints the screen.

The heartbeat: request and response. 1. You click/type. The browser sends an HTTP request to a URL. 2. DNS finds the server, Cloudflare proxies, the tunnel hands it to Finn. 3. The server runs code, usually queries the database, builds a response. 4. The browser does something with the response. - HTTP verbs that cover most of it: GET ("give me this"), POST ("here is a change, save it").

The one big fork: what the response IS. - Server-rendered (finances/workspace today): the server builds the entire finished HTML page and sends it. Click "July" -> new request -> server builds a whole new page -> browser repaints (the flash/reload). The server does the rendering. - SPA (where we are going): the first visit downloads "the app" (JavaScript) once. After that, clicking "July" calls the server's API for data only (GET /api/transactions?month=july), the server returns JSON ([{"amount":42,"date":"2026-07-01"}, ...]), and the JavaScript redraws just the table, instantly, no flash. The browser does the rendering; the server serves data.

What the back-end does: routing (URL -> function), business logic (rules), data access (SQL against Postgres), auth + respond (identity from Cloudflare -> set the RLS user -> send HTML today or JSON in the SPA).

What the database does: tables are strict spreadsheets with typed columns and links; a query (SQL) is a precise question ("June transactions for this user, newest first"); RLS makes the database itself enforce "you only see your own rows."

State (where memory lives): server-rendered pages barely remember anything (truth is in the DB, every click re-asks). An SPA holds live UI state in the browser (what is open/selected) and syncs data with the server in the background. That is why an SPA feels like an app.


5. The target architecture

5.1 The shape

One shell (sidebar + topbar + main area). Every page is a node with a type. The shell maps the type to a component and renders it in the main area.

  • Renderer registry (the core building block): a lookup table type -> component.
  • doc -> DocEditor (rich text/blocks, via an editor library)
  • database -> TableView
  • finances -> FinancesApp
  • calendar -> CalendarApp
  • habits -> HabitsApp
  • fitness -> FitnessApp, crm -> CrmApp, inbox -> InboxApp, etc.
  • A new tool = add a type + add a component. Replicable by construction.
  • Every component reads the data it needs from one shared JSON API over one Postgres.
  • Page taxonomy: doc pages (most pages), module pages (one app each: finances, calendar, habits, fitness), and the home/dashboard (a composition that pulls widgets from several modules at once: today's calendar + this week's finances + habit streaks).
  • Data is workspace-global; pages and subdomains are doors. finances.justinsforge.com becomes an alias/deep-link into the finances view of the one app, not a separate program. The friendly URL can stay; under the hood it is one codebase, one data layer.

5.2 The stack (modeled on Immich, the closest real-world template)

Layer Choice Notes
Database PostgreSQL 17 + RLS (have it) + Redis (have it) The asset. Unchanged.
Data access Drizzle (DECIDED) Type-safe, close to raw SQL, plays well with RLS (Prisma historically fights RLS).
Backend / API TypeScript, Next.js route handlers (DECIDED) One repo, shared types front-to-back. Keep existing Python as background workers for proven data jobs (finance ingest, Hevy, SimpleFIN); do not make the AI re-derive money logic.
Frontend Next.js + React + TypeScript (DECIDED) Chosen for the AI-builder lens: far more training data than Svelte (more accurate first-prompt generation), stable component patterns (Svelte 5 runes churn breaks AI builds), deepest ecosystem. Boilerplate cost is ~0 because the AI writes it.
Build tool Next/Vite toolchain New: a build step is introduced (there is none today).
Editor BlockNote (DECIDED) React-native Notion-style block editor: AI imports it and it works, vs hand-building headless Tiptap UI (thousands of lines, burns context, cascading bugs). BlockNote sits on ProseMirror, so there is an escape hatch for very custom blocks.
Auth Cloudflare Access (have it) Add nothing (no Authelia/Lucia/Auth0; redundant).
Real-time sync ElectricSQL or Yjs + WebSocket Optional/additive, for live multi-device + offline. Not day one.
Hosting LXC/systemd + Cloudflare tunnel (have it); Docker optional later Docker is not mandatory for us.
Clients Responsive web app + PWA No native app needed; PWA covers iPhone + Windows + Mac from one codebase. Native-only gaps (rich iOS push, widgets) are already covered via Telegram/Home Assistant.

5.3 What stays, changes, is new

  • Stays: Postgres 17 + lifeos schema + RLS, Cloudflare Access auth, migrations approach, most SQL logic, the homelab deploy substrate.
  • Changes: output format HTML -> JSON; code organization (clean separation: data/service layer, JSON API, typed component frontend; no more JS-in-Python-strings); the front-end is rebuilt.
  • New: a build step (Vite); optionally a real-time sync service later.

6. Why this stack (and what was rejected)

Why SPA + editor library + TypeScript: - It is what every product Justin admires is built on (Notion, Linear, Google Calendar are SPAs). - It is the most LLM-codeable stack: typed, isolated, testable components match huge training data, versus our string-templated blobs that are hard for humans and AI alike. - It unlocks the app-like features (below) and is the best path for iPhone/Windows via PWA. - One language end to end maximizes transferable learning and fits the eventual SaaS product.

Rejected, with reasons: - HTMX + server templates: a cleaner version of exactly what we do now; great for simple CRUD, but does not give the rich client state a Notion-class editor needs. It would re-create the pain. - Go / Rust backend: optimizes a constraint we do not have (idle RAM) at single-user scale on serious homelab hardware; steeper curve, less LLM-friendly. Earns its keep only at real scale. - Adding Authelia / Lucia / Auth0: auth is already solved by Cloudflare Access. - Docker as mandatory: optional; LXC + systemd + tunnels already work. - Installing a whole open-source Notion clone and grafting its database code in: the clones (AppFlowy/Rust+Flutter, AFFiNE/CRDT, Outline/React) are tightly coupled reactive apps with their own sync engines and schemas; extracting one feature is more work than building it. The reusable pieces are libraries (editor, data-grid), not a clone's subsystem.


7. What real tools are built on (reference)

Product Frontend Backend
Notion, Linear, Google Calendar/Docs, Figma, Airtable SPA (React, etc.) various
Instagram React (web) + native iOS/Android Python/Django at massive scale (proves Python scales; we change the frontend, not flee Python for speed)
TickTick JS web SPA + native mobile conventional server (likely Java)
Frame.io React Elixir/Phoenix + AWS
Immich (our template) SvelteKit Node/TS (NestJS) + Postgres + Redis + a Python ML sidecar + Docker
Home Assistant Lit (TS web components) Python core
Shopify (Gus Outdoor, Nova stores) Liquid themes / Hydrogen (React) Ruby on Rails on Google Cloud, MySQL/Vitess

Lesson: backend language varies widely; the rich frontend is always a JS/TS framework. The frontend-goes-JS decision is effectively forced; the backend is the flexible call.

Claude/OpenAI for completeness: the models are trained in Python (Anthropic JAX/TPU, OpenAI PyTorch/GPU) with C++/CUDA/XLA for the heavy math; their actual "knowledge" is learned weights (numbers), not code. Their websites are React/Next. AI is built in and trained on human code, so it is strongest where human training data is deepest (React, Python).


8. What to learn in the AI age (so the build doubles as education)

  • Commoditized (do not grind): memorizing syntax and exact API calls. AI does this instantly.
  • More valuable now: architecture/systems thinking (knowing what to build and how pieces fit), data modeling (schemas outlive frameworks; most durable skill), taste/judgment (AI averages, it has no taste), reading + verifying output (stay in control), and domain knowledge (the things only Justin knows).
  • Be T-shaped: broad conceptual literacy across the whole stack (to direct and choose well), deep in a few edges (data modeling, the chosen frontend framework, the business domains).
  • Knowing the tools and what they do is exactly what prevents the "tell an LLM to build a website and it picks the lowest-hanging fruit, then you migrate" trap. Aiming is the job.

9. The plan (phased)

Principle: keep the Postgres data layer untouched; rebuild only the app layer; migrate one module at a time; retire each old Flask surface as its replacement lands. The spine: Phases 1-3 build one complete vertical slice (data -> API -> shell -> one custom tool); Phases 4-8 repeat and enrich that proven pattern.

Phase 0 - Lock the ground - Stack is decided (section 10); only sub-choice left is Next App Router vs Vite + thin API. - Stand up an empty project, connect to Postgres read-only, prove the toolchain end to end (build, typecheck, deploy behind the Cloudflare tunnel) with one "hello data" page. - Write the conventions the AI must follow (folder layout, naming, server-vs-client component split, how identity sets RLS) so every later prompt produces consistent code.

Phase 1 - Finalize the data model - Review/clean the lifeos schema for the whole suite: nodes/collections plus the typed tables each tool needs (transactions, events, habits, contacts, tasks). - Define it once in Drizzle so the same types flow database -> API -> UI with no duplication; migrations are the single source of truth. - Validate by reading real existing data through the new layer before any UI exists, confirming the model fits what is already there.

Phase 2 - Scaffolding + JSON API - Build typed Next.js route handlers that read/write data, each setting the Cloudflare-Access identity so RLS enforces access at the database. - Ship the first endpoints (list/get/create/update for one collection) and test them directly before any screen exists. - Prove auth end to end: Cloudflare header -> API -> RLS user, verified with a "who am I, what can I see" check.

Phase 3 - Shell + renderer registry + first module - Build the shell (sidebar, topbar, main area) and the registry that maps a node's type to its component. - Build one full module (recommend finances or habits: self-contained, custom-faced) as a real view over the API, with create/edit working end to end. - This proves the whole vertical slice (DB -> API -> shell -> custom view) and becomes the template every later module copies.

Phase 4 - Port the remaining modules - Rebuild each tool (calendar, fitness, tasks, inbox, CRM, time tracker) as its own component over the shared API, one at a time. - Retire each old Flask app the moment its replacement is live, so there is never a big-bang switch. - Build the home dashboard as a composition that pulls widgets from several modules at once.

Phase 5 - Editor for docs - Drop BlockNote into the doc-page renderer so notes/pages get the Notion-style block editor immediately. - Store document content in the same data layer (blocks as JSON) so docs are queryable like everything else. - Retire the hand-rolled block editor entirely.

Phase 6 - Generalize the building blocks - Extract repeated UI into reusable components (a collection table, a date picker, a select-with-checkboxes, a property cell). - New tools assemble from these blocks instead of being built from scratch. - Document the pattern so the AI builds new tools by composing known blocks: faster, consistent, fewer bugs.

Phase 7 - Multi-tenant polish - Add workspaces, roles, and an onboarding flow on top of the RLS foundation that already isolates data. - Bring Michael in as a second real user to shake out sharing and permissions. - Prepare the per-creator path for the product, deferring the hosting model (one shared instance vs a copy per client).

Phase 8 - Real-time + offline (optional) - Add a sync engine (ElectricSQL or Yjs) so edits propagate live across Mac, iPhone, and Windows. - Add offline support via the PWA so it works without signal and syncs on reconnect. - Layer optimistic updates so the UI responds instantly while saving in the background.


10. Decisions (RESOLVED 2026-06-17)

Decided through the AI-builder lens: optimize for the highest statistical probability of the AI generating working code on the first prompt, since Justin is the architect and the AI is the developer. That metric is training-data volume + ecosystem depth + type safety + convention stability, NOT human readability.

  1. Backend language: TypeScript (Next.js route handlers in one repo), with Python kept as background workers for proven data jobs (finance ingest/categorization, Hevy, SimpleFIN). Rationale: end-to-end shared types catch AI mistakes at compile time; do not make the AI re-derive money logic that already works in Python.
  2. Frontend framework: Next.js + React + TypeScript. Rationale: vastly more training data than Svelte = more accurate first-prompt generation; React component patterns are stable while Svelte 5 runes churn causes AI to mix syntax and break builds; deepest ecosystem; boilerplate cost is ~0 because the AI writes it.
  3. Editor: BlockNote (React-native, ProseMirror underneath). Rationale: AI imports it and the Notion-style editor works; the alternative (hand-building headless Tiptap UI in SvelteKit) is thousands of lines of editor-state logic that burns AI context and breeds cascading bugs.
  4. Data access: Drizzle. Database: Postgres 17 + RLS (unchanged). Auth: Cloudflare Access (unchanged).

Remaining sub-decision (Phase 0): Next App Router (pinned version) vs Vite + React + a thin typed API. Default to Next App Router; the one thing to verify carefully is the server-vs-client component boundary, where AI most often errs. Fall back to Vite+React if that boundary fights us.

The judgment layers stay Justin's to direct and verify: the data model and the money logic. Framework choices make the easy 80% reliable; the hard 20% still needs the architect.


11. Guardrails to hold (do not violate)

  • The data is the asset. Never couple it to a page, a URL, or a framework. Workspace-global, RLS-enforced.
  • Custom faces, not generic chrome. Each tool gets a bespoke view; the database widget is a fallback, not the goal.
  • Migrate incrementally. Never a big-bang rewrite; one module at a time, old retired as new lands.
  • Security (non-negotiable, from doctrine): secrets only in ~/.forge-secrets/*.env; never in git; passwords hashed; Cloudflare Access in front; RLS enforced; confirm before destructive or outward-facing actions.

12. Canonical naming (glossary) - DECIDED 2026-06-17

Because the build is driven by natural language, every distinct thing has exactly one canonical name, and no two things share a name. Use these exact words when instructing the AI.

Concept Canonical name Notes / do-not-confuse
The whole product (everything at justinsforge.com) The Suite Repo forge-suite. NOT "workspace."
The database (where all data lives) lifeos Postgres database/schema name. "Data layer"/"spine" are descriptions of its role, NOT names; say lifeos.
The per-user/per-creator tenant container workspace Justin has one; Michael gets one; each creator gets one (multi-tenancy). NOT the product.
A single tool inside The Suite module Generic term; specific ones named below.
A specific module its own short name = its subdomain clips, finances, fitness, calendar, habits, tasks, inbox, crm, time-tracker. Module name always equals subdomain (clips = clips.justinsforge.com).
A page in the product page (in conversation/UI) Schema table stays app.nodes; say "node" only about the schema, "page" everywhere else.
Content inside a doc page block
A typed table of rows (finances rows, contacts, etc.) collection
The Postgres LXC container (infra) forge-data (CT 109) Doctrine kebab-case forge-<function>. Holds the lifeos database + Redis.
The new app repo (infra) forge-suite At /home/justinwieb/forge-suite.

Rule of thumb when giving instructions: product = The Suite; database = lifeos; a tenant = workspace; a tool = a module (named like clips); a page = page. Never use "workspace" to mean the product, and never use "data layer" as if it were the database's name.


[Claude Code]