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) onforge-dataLXC + 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
ownerrole 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 indocsschema). - 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.pybefore 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.dbreadable 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)¶
- Phase order: migrate finance AFTER the Notion-replacement domains are proven (Phase 3). Confirmed.
- Redis: same LXC as Postgres for v1. Confirmed.
- Docs: must be as Notion-like as possible, as many features if not more. So
docsschema = nested blocks/pages (block model:doc_blockswith parent_id, type, content, order), not simple title+body. This is a first-class build target, not an afterthought. - Calendar: build our own native calendar in-app (not Google embed). Justin will still use his phone's calendar app alongside it. Add
calendarschema (eventswith workspace_id, start/end, recurrence, attendees). - v1 workspaces: confirmed 6 (Personal, Nova, JustinWieb, Gus Outdoor, Gus The Bass, Sip).
- 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.