Mirrors the mbrian_admin pattern: the binary connects as a role bounded
to the projax schema, so even a compromised projax process cannot read
mai.workers, otto.*, vault.*, etc.
- 0001: switch grants block from postgres → projax_admin (conditional
on the role existing — bootstrap still works as superuser before the
role is created). Wrap `create schema` in a guard so the migration
is idempotent when re-run as a non-superuser app role that lacks
database-level CREATE.
- 0005_reown_to_projax_admin.sql: enumerate every projax-namespaced
object via pg_namespace + pg_class / pg_proc and ALTER OWNER to
projax_admin. Explicitly scoped — no global REASSIGN OWNED that
would yank ownership from other projects sharing the postgres role.
Strips residual postgres grants. No-ops with a NOTICE when the role
is missing.
- README: new "Manual prerequisite" deploy section. Documents the
CREATE ROLE statement, the cross-schema USAGE + SELECT grants, AND
the RLS policy `projax_read ON mai.projects` that's required because
mai.projects has row-level security with policies scoped to `mai`
and `anon` only. Without the policy, items_unified silently returns
zero mai-source rows.
- deploy/dokploy.yaml: DSN comment now reflects projax_admin and
points at the README prereq.
Verified locally against msupabase with a throwaway projax_admin role:
- 13/13 tests green
- mai.workers SELECT → permission denied
- mai.sessions SELECT → permission denied
- mai.projects SELECT → 59 rows (RLS policy in effect)
- projax.items_unified SELECT → 66 rows (7 projax + 59 mai)
- Drop the "Open design questions (Phase 1)" section — answered in
docs/design.md.
- Replace "Tech stack: TBD by inventor" with the actual stack
(Go + pgx + html/template + HTMX, msupabase schema projax, Dokploy).
- Replace "Worker preferences: inventor / coder shifts" with the
current state (PRD landed, Phase 1 implementation underway, Phase 2
and 3 deferred per docs/design.md).
- Point readers at docs/design.md as the live spec, and add a layout
map so a fresh agent knows where each concern lives.
No code touched; this is the last commit in the Phase 1 branch before
merge to main per head's review (msg #1775).
- Multi-stage Dockerfile: golang:1.25-alpine builder → distroless static
runtime as nonroot. Image weighs ~15 MB. Embeds templates, static
assets and migrations into the single binary.
- deploy/dokploy.yaml documents the Dokploy app for projax.msbls.de:
Tailscale-only, healthz path, single replica, secret PROJAX_DB_URL.
Translates to the Dokploy UI; not auto-applied.
- README rewritten as runbook: env vars, route table, test command,
deploy notes, trust model (Tailscale + no auth in v1, defer to
Supabase auth if it ever outgrows the fence), schema summary.
- .dockerignore strips .git, .m, .claude, docs, tests from build ctx.
- .gitignore covers ad-hoc binary and dist artefacts.
Verified locally: docker build succeeds, container responds to /healthz
and / against msupabase via --network host.
cmd/projax/main.go boots a pgxpool against PROJAX_DB_URL (falls back to
SUPABASE_DATABASE_URL), auto-applies embedded migrations on start
(disable with PROJAX_AUTO_MIGRATE=off), and serves on PROJAX_LISTEN_ADDR
(default :8080).
store package wraps the unified view + projax.items writes. Item has
helper methods for templates: IsArea, Editable, SourceRefDeref. The
Promote() flow runs the insert + item_links link inside a single
transaction so the source row drops out of items_unified atomically.
web package: per-page html/template instances parsed against a shared
layout.tmpl, embedded static/style.css, HTMX from CDN. Pages:
GET / tree of items_unified
GET /i/{path} detail (editable for projax, read-only +
promote form for mai.projects)
POST /i/{path} update projax-native item
POST /i/{path}/promote one-page promote (HTMX-aware fragment for
inline classify)
GET /new?parent={path} create form
POST /new create projax-native item
GET /admin/classify orphan list with inline HTMX promote
GET /healthz DB ping
GET /static/* embedded assets
Auth is intentionally out of scope for v1 — service binds to whatever
PROJAX_LISTEN_ADDR points at, deploy guidance pins it to the Tailscale
interface (covered in 1d README).
Tests (skip when DB env is unset):
TestTreeRenders, TestHealthz,
TestDetailProjaxNativeEditable, TestDetailMaiProjectsReadOnly,
TestClassifyListsOrphans, TestPromoteRoundTrip.
projax.items_unified joins projax.items (deleted_at IS NULL) with
mai.projects so a single query feeds the tree UI. mai.projects.id is a
text key, so a deterministic placeholder UUID is derived from md5(p.id);
projax-native rows keep their gen_random_uuid().
When a projax item is created with an item_links row pointing back to a
mai.projects id (ref_type='mai-project'), the corresponding mai.projects
row drops out of the view — that's how the "Promote to projax" flow
makes the duplicate disappear without ever touching mai.projects.
Test coverage:
- both sources appear in the view
- promotion link hides the mai source row and surfaces the projax row
- 0001_init.sql: projax.items + projax.item_links tables with indices,
partial-unique root slug, updated_at trigger, schema grants to the
application role.
- 0002_path_trigger.sql: BEFORE-write trigger maintains items.path via
recursive parent walk; rejects cycles and structural-rule violations
(areas at root, projects not at root). AFTER trigger rewrites
descendant paths on slug rename or re-parent.
- 0003_seed_areas.sql: dev, sports, home, work, health, finances, social.
- db/migrate.go: embed.FS-backed sequential runner.
- db/migrate_test.go: integration suite covering idempotency, nest,
rename propagation, re-parent propagation, cycle rejection, and
structural rules. Skips when no DB env var is set.
Also ignores .m/events.log and .m/locks (per-worker scratch).
Copy the design PRD, .claude config, .m config, .mcp.json, and AGENTS.md
symlink from m's main working tree so the worker has the full project
context before starting Phase 1 implementation.