Skip to content

forge-data / core database spine (CT 109, formerly "lifeos")

URL: https://mkdocs.justinsforge.com/memory/general/reference_forge_data_lifeos_spine/

CANON UPDATE (2026-06-18) , "lifeos" retired, this is now "the core database" (core). This Postgres spine (CT 109, forge-data) is the data layer for CreatorTrack (product formerly "The Suite"/"Workspace"), built and previewed at dev.creatortrack.ai. Vocabulary: stop calling it "lifeos"; call it the core database / core. The physical DB name lifeos + roles lifeos_app/lifeos_admin -> core is a SCHEDULED migration (invisible internal identifiers, downtime risk), NOT done yet, so the current connection strings/roles still read lifeos until that migration runs. "workspace" survives only as the internal RLS data-tenant term. Full canon (with tech-stack overview): creator-suite-vision-strategy.

The Postgres data spine for the workspace app + life/business data unification. Build plan: workspace-lifeos-spine-build-plan-2026-06-15. Status (2026-06-15): Phase 0 + 1 COMPLETE; Phase 2 domains BUILT. RLS gate passing. Phase 2 domains (tasks, projects, habits, docs, inbox) live + RLS-enforced. DB spine stays canonical (it is CreatorTrack's data layer); only the old Flask app surface below was retired.

DECOMMISSIONED 2026-07-04 , the old workspace.justinsforge.com Flask app. The forge-workspace.service user unit was disabled + its unit file removed, and the workspace.justinsforge.com DNS record + VR Alliance ingress rule were deleted. That surface was an orphaned read-view over the shared core/lifeos Postgres (rendered stale legacy app.meal_entries rows). CreatorTrack (forge-suite) is the only live product surface now. The DB spine, RLS core, roles, and forge-workspace-backup timer (pg_dump -> restic -> gdrive) all stay live. Bot capture CUTOVER LIVE 2026-06-15: WORKSPACE_WRITE_TARGET=postgres set on forge-inbox-capture / forge-inbox-webhook / forge-brain-server (systemd drop-ins /etc/systemd/system/<svc>.service.d/cutover.conf); captures write to the spine, not Notion. No data migrated (Justin chose fresh start); 7 starter habits seeded in Personal. Remaining: deactivate the n8n notion-create/update/query workflows (2.5) + Phase 3 (finance/commerce/context migration). Rollback = set the drop-in flag to notion, restart the three services.

Venv dependency (load-bearing): the bots run in their own venvs (~/.forge-venvs/inbox, ~/.forge-venvs/brain-server), NOT system Python. forge_workspace_capture needs psycopg[binary,pool] installed in BOTH, or the spine import fails and capture silently falls back to Notion (the cutover no-ops). Installed 2026-06-16. Any new venv that runs the brain needs psycopg too.

CANON UPDATE (2026-07-01), capture repointed to CreatorTrack. forge_workspace_capture.py (same bot-facing signatures) now POSTs to the CreatorTrack capture API (/api/capture on :3060, bearer CAPTURE_TOKEN in ~/.forge-secrets/creatortrack-auth.env, acts as Justin via core.login()). Data lands as COLLECTION ROWS in the app.nodes engine (system collections: Habits Definitions/Log, Tasks, Projects, Inbox; provisioning in forge-suite/lib/system-apps.ts). The Phase 2 domain tables below (habits./tasks./inbox. schemas) AND the interim app.tasks/app.task_lists are FROZEN ORPHANS, do not extend them. Build plan: creatortrack-lifeos-apps-build-2026-07-01.

Phase 2 domains (migrations 0004-0007)

  • tasks (projects, tasks), habits (habit_defs, habit_logs), docs (docs, doc_blocks nested block model), inbox (items). Each table workspace_id-scoped via core.apply_workspace_rls(). Blueprints: forge_workspace_ui_tasks/_projects/ _habits/_docs/_inbox.py, registered in the app, nav under /w//
    .
  • forge_workspace_capture.py: canonical spine capture API (create_task/ save_to_inbox/log_habit) with bot brand -> workspace map.
  • Bot cutover flag WORKSPACE_WRITE_TARGET in forge_telegram_inbox_brain.py: notion (default) | dual | postgres. To cut over: set the bot service env to dual, restart forge-brain-server + forge-inbox-capture, watch captures land in the app, then postgres, then retire the n8n notion-create/update/query workflows (keep the Alexa bridge). Rollback = set flag back to notion. Export Notion to JSON before retiring writes.

Host

  • CT 109 forge-data, Debian 13, 192.168.86.86, 4 cores / 4GB / 40G. Admin via ssh finn 'pct exec 109 -- ...' (no SSH key inside the container by design; app reaches it over TCP only).
  • Postgres 17, DB lifeos, UTF8 / C.UTF-8 (the cluster's template DBs are SQL_ASCII; lifeos was rebuilt from template0 as UTF8 -- never recreate it as SQL_ASCII or psycopg returns text as bytes). pgvector installed AND marked trusted = true in vector.control so the non-superuser admin can create it.
  • Redis on the same LXC, bound 127.0.0.1 192.168.86.86, password-protected.

Roles (the RLS security model)

  • lifeos_app: the role the Flask app uses. NOSUPERUSER, NOBYPASSRLS, non-owner -> every RLS policy binds to it. Identity is set per request via set_config('app.user_id', ...) + set_config('app.is_owner', ...).
  • lifeos_admin: owns schemas/tables (bypasses RLS as owner). Runs migrations, seed, ETL, backup only. CREATEDB (for restore-verify). The app must never open an admin connection.
  • pg_hba: only Console (192.168.86.50/32), scram-sha-256. lifeos_app scoped to db lifeos; lifeos_admin to all (needs the postgres maint DB for createdb).

RLS contract

Policies in data/workspace/migrations/0002_rls.sql. Helpers core.current_user_id(), core.is_owner() read the GUCs (default deny if unset). core.apply_workspace_rls(regclass) is the reusable applier every Phase 2+ workspace-scoped table calls once. Core tables workspaces/memberships/users already have RLS (member sees only their own; owner sees all).

Scripts (registered tools)

  • scripts/forge_workspace_db.py -- canonical connection helper (app pool + admin/app direct conns + set_identity + app_request context manager).
  • scripts/forge_workspace_migrate.py -- migrations runner over data/workspace/migrations/NNNN_*.sql, tracks core.schema_migrations. Idempotent.
  • scripts/forge_workspace_seed.py -- seeds Justin (owner) + the 6 workspaces (personal, nova, justinwieb, gus-outdoor, gus-the-bass, sip). Idempotent.
  • scripts/forge_workspace_rls_test.py -- HARD GATE. Proves a scoped member cannot read another workspace's rows (core + domain probe), owner god-view, anonymous-deny, no app-role escape hatch. Exit 0 = pass.
  • scripts/forge_workspace_app.py -- Flask app, formerly served 0.0.0.0:8101 (user service forge-workspace.service). RETIRED 2026-07-04 (service disabled, unit file removed, DNS + ingress deleted). Historical detail below. Resolves the Cloudflare Access email header -> core.login() (SECURITY DEFINER, migration 0003) -> per-request identity -> RLS. Dev-email fallback is HARD-GATED to local/Tailscale hosts; the public host with no Access header gets 401 (no owner leak). PWA (manifest + service worker + gradient icon). Routes: /, /w/<slug>, /w/<slug>/<section> (Phase 2 stubs), /health. Design system in scripts/forge_workspace_ui_base.py (sidebar + workspace switcher, lifted from finances).
  • Cloudflare: workspace.justinsforge.com DNS + VR Alliance ingress rule DELETED 2026-07-04. (Historically rode tunnel fb20b500 -> localhost:8101 behind Access app workspace.) The VR Alliance tunnel still serves finances.
  • scripts/forge_workspace_backup.sh [--verify-restore] -- pg_dump (custom) -> restic rclone:gdrive:forge-backup (tag lifeos-spine), with a round-trip restore-into-throwaway-DB parity check. Nightly via forge-workspace-backup.timer (user systemd, 03:45). Needs pg-client-17 on Console (installed from PGDG; pg16's pg_dump refuses a v17 server).

Secrets

~/.forge-secrets/forge-data.env (chmod 600): PG app + admin creds, Redis creds.