Skip to content

Build Plan: Workspace app + lifeos data spine

URL: https://mkdocs.justinsforge.com/memory/plans/workspace-lifeos-spine-build-plan-2026-06-15/

Owner: Justin. Drafted 2026-06-15. Status: Phase 0 + 1 COMPLETE; Phase 2 domains BUILT 2026-06-15. GATE PASSING; app live behind Cloudflare Access; tasks/projects/habits/docs/inbox domains live + RLS-enforced; bot capture wired behind WORKSPACE_WRITE_TARGET (default Notion). Pending: operational bot cutover (flip flag, export Notion, retire n8n proxy = 2.5) + Phase 3 (finance/commerce/context migration). See reference_forge_data_lifeos_spine. Architecture decision of record: memory/project_lifeos_data_spine.md (Claude project memory).


1. Goal

Stand up one Postgres data spine (lifeos on a new forge-data LXC) as the single source of truth for all structured life + business data, and build a custom Flask app (workspace.justinsforge.com) as the human surface, with Notion-style workspaces, multi-user logins, and an owner god-view for Justin. Migrate existing SQLite silos (finance.db, datalayer.db, context.db) in; keep ERPNext as the downstream books/tax engine; retire Notion-direct writes.

2. Locked decisions (from the design conversation)

  • Surface: custom-built, NOT Directus/NocoDB/AppFlowy. No interim tools.
  • Stack: Flask + blueprints + server-rendered HTML, matching finances.justinsforge.com (forge_finances_app.py + forge_finances_ui_*.py). Python, flat verbose filenames per doctrine.
  • Data: one Postgres (lifeos) on forge-data LXC + pgvector. Redis beside it for cache/scratch.
  • Multi-tenancy: one app, workspace switcher. Workspaces: Personal, Nova, JustinWieb (Justin only) + Gus Outdoor, Gus The Bass, Sip-N-Serve (multi-user). One physical DB holds ALL workspaces' data; separation is logical via workspace_id + row-level security.
  • Owner god-view: Justin is a member of every workspace with owner role that bypasses scoping. Michael scoped to Gus workspaces, Kristine to Sip.
  • Permissions: Cloudflare Access (front door) + Postgres Row-Level Security (DB-level walls). Justin owns this layer.
  • Build vs adopt: build everything except books/tax. ERPNext stays, fed from the spine via existing forge_erpnext_daily_post.py (repoint source only).
  • Words vs rows: Postgres holds rows. memory/*.md, system-map, code stay in Git. User-facing docs/notes are an in-app feature (rows in docs schema).
  • Backup: offsite encrypted backup (Backblaze B2 or Hetzner) wired BEFORE the spine is depended on.

3. Naming + conventions

Thing Name
LXC / host forge-data
Postgres database lifeos
Subdomain workspace.justinsforge.com
App entry scripts/forge_workspace_app.py (Flask, thin, registers blueprints)
Feature blueprints scripts/forge_workspace_ui_tasks.py, _projects.py, _habits.py, _docs.py, _calendar.py, _inbox.py
DB helper (canonical) scripts/forge_workspace_db.py (connection pool, per-request app.user_id set_config)
Migrations runner scripts/forge_workspace_migrate.py + data/workspace/migrations/NNNN_*.sql
Secrets ~/.forge-secrets/forge-data.env (chmod 600)

4. Data model (v1)

Schema core: - users (id, email, display_name, is_owner bool, created_at) - workspaces (id, slug, name, kind[personal|business], created_at) - memberships (user_id, workspace_id, role[owner|admin|member|viewer], created_at)

Domain schemas (every table carries workspace_id): - tasks: projects (id, workspace_id, name, status, ...), tasks (id, workspace_id, project_id, title, status, due_date, assignee_id, ...) - habits: habit_defs, habit_logs - docs: docs (id, workspace_id, title, body, parent_id for nesting) - contacts: people, interactions (CRM) - finance: migrated from finance.db - commerce: migrated from datalayer.db - context: migrated from context.db (wellness/home facts) - search: pgvector embeddings (or embedding columns per table) for /recall

Row-Level Security (the security spine): - Every domain table: RLS policy USING (workspace_id IN (SELECT workspace_id FROM core.memberships WHERE user_id = current_setting('app.user_id')::int) OR current_setting('app.is_owner')::bool). - App sets app.user_id + app.is_owner per request via set_config after Cloudflare Access identifies the user. - DB enforces walls even if app logic has a bug.

5. Phased build (numbered, with verification + commit boundaries)

Phase 0 , Provision forge-data (foundation, serial)

0.1 Create forge-data LXC on Finn (per fleet conventions, static IP, Tailscale per policy). 0.2 Install Postgres (latest stable), create lifeos DB, enable pgvector (CREATE EXTENSION vector;). 0.3 Install Redis (same LXC for v1; split later if needed). 0.4 Cloudflare tunnel + DNS for workspace.justinsforge.com; Cloudflare Access policy (allow Justin; Michael/Kristine added in Phase 5). 0.5 Secrets to ~/.forge-secrets/forge-data.env. 0.6 Offsite backup FIRST: nightly pg_dump (or pgBackRest) -> restic -> Backblaze B2/Hetzner, encrypted. Verify a restore to a throwaway DB before proceeding. - Verify: psql connects to lifeos; pgvector present; backup runs + test restore succeeds. - Commit: infra units + backup script + secrets template (no secrets).

Phase 1 , Core schema, multi-tenancy, auth, app skeleton (serial; security-critical)

1.1 forge_workspace_db.py connection helper + per-request identity. 1.2 Migrations 0001_core.sql (users, workspaces, memberships) + 0002_rls.sql (policies, owner bypass). 1.3 forge_workspace_app.py Flask skeleton; Cloudflare Access JWT/header -> app.user_id. 1.4 Seed Justin as owner; seed the 6 workspaces. 1.5 Workspace switcher UI + base template (lift CSS/JS pattern from forge_finances_ui_base.py). - Verify: Justin logs in, sees all 6 workspaces. RLS test: simulate a member user; confirm they CANNOT read another workspace's rows (automated test in forge_workspace_rls_test.py). This gate must pass before any real data or second user. - Commit: core schema + RLS + app skeleton + passing RLS test.

Phase 2 , Notion-replacement domains (the headline migration)

2.1 tasks + projects schema + blueprints (_tasks.py, _projects.py). 2.2 habits schema + blueprint (port logic from forge_habits_*). 2.3 docs schema + blueprint. 2.4 Repoint Telegram bots (forge_telegram_brain.py, forge_telegram_inbox_brain.py) to write Postgres directly; remove n8n notion-create/update/query calls. 2.5 Retire n8n Notion proxy workflows (keep Alexa bridge). - Verify: capture via bot lands in lifeos, appears in app; habits roll over correctly; Notion no longer receives writes. Old Notion data exported to JSON for safety. - Commit: per domain (tasks, habits, docs) + bot repoint.

Phase 3 , Migrate finance/commerce/context into the spine

3.1 ETL finance.db -> finance schema (preserve ids; parity row-count check). 3.2 Repoint finances.justinsforge.com (forge_finances_*) to read lifeos.finance. 3.3 Repoint forge_erpnext_daily_post.py SOURCE from finance.db to lifeos.finance (target ERPNext unchanged). 3.4 ETL datalayer.db -> commerce, context.db -> context. - Verify: finance app renders identically off Postgres; ERPNext still posts (idempotent, no dupes); row counts match; check data/finance/COORDINATION.md before schema touches. - Commit: per source migrated + consumer repoint.

Phase 4 , pgvector / recall

4.1 Embedding columns/table; backfill embeddings across docs/tasks/notes. 4.2 Point /recall at pgvector. - Verify: semantic search returns cross-domain hits from the spine.

Phase 5 , Multi-user rollout

5.1 Add Michael (member: Gus Outdoor, Gus The Bass), Kristine (member: Sip) to memberships + Cloudflare Access. 5.2 Google Calendar native sharing for brand calendars; app displays. 5.3 Confirm owner god-view + per-member scoping live. - Verify: Michael sees only Gus workspaces; Justin sees all; RLS holds with real users.

Phase 6 , Cut Notion

6.1 Decommission Notion-direct paths; archive Notion export. 6.2 Decide n8n fate (retire CT 106 if Alexa relocated).

6. Risks + mitigations

  • Cross-workspace data leak (highest): DB-level RLS + mandatory passing forge_workspace_rls_test.py before any second user. Never gate privacy on app code alone.
  • Finance migration touches live app + ERPNext poster: parallel-run, parity checks, COORDINATION.md, keep finance.db readable until parity confirmed.
  • Depending on spine before backup: Phase 0.6 hard-gates this.
  • Notion cutover: export to JSON before retiring writes; keep until confident.
  • Bot double-write during transition: feature-flag bot write target (Notion vs Postgres) for clean cutover.

7. Agent deployment

  • Serial foundation: ONE agent owns Phase 0 + 1 (infra + schema + RLS + skeleton). This is security-critical and must not be parallelized.
  • Parallel after Phase 1 gate passes: spawn up to 2 agents:
  • Agent A: Phase 2 domain UIs (tasks/projects/habits/docs) + bot repoint.
  • Agent B: Phase 3 data migration (finance/commerce/context) + consumer repoint.
  • Phases 4-6 sequence after 2 + 3 land.

8. Resolved by Justin (2026-06-15)

  1. Phase order: migrate finance AFTER the Notion-replacement domains are proven (Phase 3). Confirmed.
  2. Redis: same LXC as Postgres for v1. Confirmed.
  3. Docs: must be as Notion-like as possible, as many features if not more. So docs schema = nested blocks/pages (block model: doc_blocks with parent_id, type, content, order), not simple title+body. This is a first-class build target, not an afterthought.
  4. Calendar: build our own native calendar in-app (not Google embed). Justin will still use his phone's calendar app alongside it. Add calendar schema (events with workspace_id, start/end, recurrence, attendees).
  5. v1 workspaces: confirmed 6 (Personal, Nova, JustinWieb, Gus Outdoor, Gus The Bass, Sip).
  6. Scope: do NOT defer domains, but no rush, take our time and build each properly.

9. Hard acceptance criteria (Justin)

  • finances.justinsforge.com must end byte-for-byte identical in behavior/appearance after migrating to the spine. A few days of downtime DURING the build is acceptable; the END STATE must match.
  • ERPNext books must end identical. Posting engine repoint must produce the same Journal Entries.
  • Rollback baseline captured pre-build (2026-06-15):
  • Data: data/_prebuild-snapshots/finance.db.prebuild-workspace-2026-06-15 + erpnext_post_state.db.prebuild-workspace-2026-06-15
  • Code: git ref cbee723
  • Visual: logs/site-screenshots/shot-20260615-143530-{desktop,mobile}.png (dashboard). Capture remaining finance sub-pages before Phase 3 cutover for full visual diff.

10. Mobile (added , Justin's "one app for everything" thread)

  • v1 = PWA. Make the Flask app a Progressive Web App (manifest + service worker): "Add to Home Screen" gives a full-screen app icon on iPhone/Android, offline shell, and web push. ~Zero extra stack, same codebase, covers ~90% of "feels like an app."
  • Capture already mobile-solved via existing Telegram bots; keep them as the low-friction capture path.
  • Native app (iOS) = later, optional. Only if specific native features are wanted: home-screen widgets, iOS share-sheet capture (share any link/photo INTO a workspace), Siri/Shortcuts, biometric lock, background sync. Defer past v1; revisit once the web app is real.