Plan: Shared Postgres (CloudNativePG)
Vision
Transform pal-e-docs from a note storage app into an AI-native knowledge engine.
Act 1 — Enterprise Postgres (Phases 1-4, COMPLETED): Migrate from SQLite to CloudNativePG on k3s. Platform provides the operator and shared infra. Apps own their database lifecycle — Cluster CRDs, credentials, backups, and migrations live in app repos, deployed by ArgoCD. This eliminated production outages from SQLite's DDL handling and laid the foundation for everything that follows.
Act 2 — Knowledge Engine (Phases 5-8, ACTIVE): Today, AI agents interact with platform knowledge by brute force — enumerate notes, fetch full HTML blobs, reason over thousands of tokens to find one answer. That's unsustainable. Act 2 builds the intelligence layer: full-text search so agents can find knowledge instead of enumerating it, semantic search so they can find related knowledge even without exact terms, structured content so knowledge is queryable at the block level, and optimized MCP tools that exploit all of it. The end state: any agent can ask a question and get a precise, ranked answer with context — without reading every document in the system.
Why Now
Act 1 trigger: Two production outages from SQLite's auto-committed DDL crashing Alembic migrations. Sprint tables (plan-2026-03-01-pal-e-sprints) added another migration. Postgres eliminated the root cause.
Act 2 trigger: Act 1's completion enables Act 2. With Postgres live, we have access to tsvector, pgvector, GIN indexes, triggers, and the full SQL query engine. The real problem is now exposed: 246 notes stored as HTML blobs with no search capability. Every knowledge lookup burns 12+ MCP calls and thousands of tokens. Session context injections alone consume a significant fraction of the context window. As the knowledge base grows, this gets worse — linearly more tokens per query, linearly more calls per search. Search-first architecture inverts that: one query, ranked results, snippets. Estimated 80-90% token reduction per knowledge interaction.
Architecture Revision (2026-03-02)
Original plan put everything in pal-e-platform/main.tf — operator, Cluster CRD, secrets, backups. This caused:
- Terraform + CRD friction —
kubernetes_manifestprovider can't handle CNPG webhook mutations (32 injected params, broke on every apply) - Wrong ownership — platform repo owned app-level concerns (database config, credentials, backup schedules)
- Cross-repo migrations — Alembic lives in pal-e-docs but database definition lived in pal-e-platform
New pattern: platform provides capability, apps consume it.
| Layer | Repo | Deployed by | Resources |
|---|---|---|---|
| Platform | pal-e-platform | Terraform | CNPG operator (Helm), postgres namespace, MinIO bucket + IAM, S3 creds secret |
| App | pal-e-docs + deployments | ArgoCD | Cluster CRD, SOPS-encrypted secrets, ScheduledBackup, Alembic migrations |
Decisions Made
| Decision | Rationale |
|---|---|
| CloudNativePG operator | CNCF project. K8s-native CRDs, automated failover, built-in WAL archiving, PgBouncer integration. |
| Per-app Cluster CRDs, platform provides operator | Each app defines its own CNPG Cluster in its repo. Platform installs operator + shared infra. |
| ArgoCD deploys app-level CRDs | ArgoCD tolerates webhook mutations naturally. No Terraform CRD friction. |
| SOPS + Age for CNPG secrets | Encrypted in Git, decrypted at deploy by ArgoCD. Reproducible. See <code>sop-secrets-management</code>. |
| WAL archiving to MinIO | Continuous backup with point-in-time recovery. |
| Short maintenance window for SQLite cutover | Sessions fail-open. One-time migration, dual-write not worth the complexity. |
| Qwen3-Embedding-4B via Ollama | GPU-accelerated on GTX 1070. Near-8B quality, instruction-aware, 768 dims. See <code>decision-phase6-vector-search-architecture</code>. |
| Per-block embedding (not per-note) | Section-level semantic search. Requires Phase 7 blocks before Phase 6 vectors. |
| SDK-first MCP architecture | API → SDK → MCP. Integration tests at SDK layer. Proven with woodpecker-sdk, forgejo-sdk. Phase 8 pulled forward — doesn't need Phase 6. |
Projects & Repos Touched
| Project/Repo | Platform | Role |
|---|---|---|
| pal-e-platform | Forgejo | CNPG operator Helm release + shared infra only. Ollama deployment (Phase 6). |
| pal-e-docs (app) | Forgejo | Cluster CRD, secrets, backup, SQLAlchemy + Alembic, search endpoints, blocks API |
| pal-e-docs-sdk | Forgejo | Typed Python client for pal-e-docs API. Published to Forgejo PyPI v0.2.0. Phase 8. |
| pal-e-docs-mcp | Forgejo | MCP tools — rewritten in Phase 8f to wrap SDK instead of raw httpx. v0.2.0 live. |
| claude-custom | Forgejo | Claude Code config — hooks, agent personalities, session injection. Phase 7e-3. |
| forgejo_admin/deployments | Forgejo | Kustomize overlay with SOPS-encrypted CNPG secrets |
| # | Phase | Status | Slug |
|---|---|---|---|
| {"html" => "<strong>Act 1 — Enterprise Postgres</strong>", "colspan" => 4} | |||
| 1 | TF Modularization | DEFERRED | <code>phase-postgres-1-tf-modularize</code> |
| 2 | Platform CNPG Foundation | COMPLETED | <code>phase-postgres-2-deploy-cnpg</code> |
| 2b | Clean Up Platform TF | COMPLETED | <code>phase-postgres-2b-cleanup-platform</code> |
| 3 | pal-e-docs Owns Its Postgres | COMPLETED | <code>phase-postgres-3-migrate-pal-e-docs</code> |
| 4 | Backup Verification + Restore SOP | COMPLETED | <code>phase-postgres-4-backup-restore</code> |
| {"html" => "<strong>Act 2 — Knowledge Engine</strong>", "colspan" => 4} | |||
| 5 | Full-Text Search (tsvector) | COMPLETED | <code>phase-postgres-5-fulltext-search</code> |
| 7 | Block-Structured Content Model | COMPLETED (7a-7d) | <code>phase-postgres-7-block-content</code> |
| 8 | SDK + MCP Rewrite + Integration Tests | COMPLETED (8a-8g) | <code>phase-postgres-8-mcp-optimization</code> |
| 7e | Compiled Page Architecture | COMPLETED | <code>phase-postgres-7e-compiled-pages</code> |
| 6 | Vector Search (pgvector) | COMPLETED (6a-6e) | <code>phase-postgres-6-vector-search</code> |
| 7f | Doc Cleanup + SOP Hardening | COMPLETED | <code>phase-postgres-7f-doc-cleanup-sop</code> |
| {"html" => "<strong>Epilogue</strong>", "colspan" => 4} | |||
| E | Post-Plan Cleanup | IN PROGRESS (items 1,2,3,6,9,11 resolved) | <code>phase-postgres-epilogue-cleanup</code> |
Dependency Chain
Lessons Learned
kubernetes_manifest+ CNPG webhook = broken. Webhook injects 32 default params, provider errors on every apply.- Dev agents must run
tofu planfor TF changes — format/validate is not enough. - CRD resources belong with the apps that consume them, not in platform Terraform.
kubectl port-forwardunreliable on k3s (CNI drops connections). Usekubectl cp+kubectl execfor DB operations.- ArgoCD Image Updater creates ghost overrides (
.argocd-source-*.yaml) that silently pin image tags. Add to.gitignore. Seeconcept-argocd-ghost-override. - Squash merge SHA ≠ branch SHA. Woodpecker
${CI_COMMIT_SHA}is the merge commit on main. Seeincident-phase5-deployment-outage-2026-03-06. - Pod env var is
PALDOCS_DATABASE_URL, notDATABASE_URL. Backfill script needs:sh -c 'DATABASE_URL="$PALDOCS_DATABASE_URL" python /tmp/backfill_blocks.py' - Always run
ruff format --checkafter manual nit fixes — CI catches it but by then the deploy is blocked. - Claude Code hook
permissionDecisionvalid values:allow,deny,ask. Invalid values cause silent fail-open. Seebug-merge-hook-silent-error. - Convention shifts need more than code — 7e-3 became 4 deliverables (convention note, agent personalities, SOP, hook). Patterns must be encoded everywhere agents get instructions.
- Merged ≠ deployed ≠ data consistent — 7e-1 fixed future writes but 58 gap notes had no blocks. Always verify data consistency after schema changes.
- Plan broadly, execute narrowly — 7f-4 session compressed 3 planned subphases (7f-4, 7f-5, 7f-6) into one through aggressive parallelization. Plan structure defines scope; execution finds natural parallelism.
- k3s nvidia runtime is NOT default — pods must set
runtimeClassName: "nvidia"explicitly. Without it, NVML can't discover the GPU and device plugin reports 0 capacity. (Phase 6a, PR #27) - Forgejo auto-closes issues when PR body contains
Closes #N— enabled by default since Gitea. 19 stale issues accumulated because Dev agents never used this keyword. Enforce viacheck-pr-template.shhook. (Phase 6c-1)
Concept Notes
concept-phase5-database-side-intelligence— why we put intelligence in Postgres, not application codeconcept-phase5-self-hosted-rag— how Act 2 builds a self-hosted RAG systembenchmark-phase5-knowledge-baseline— baseline measurements before search (11 calls / ~44K chars / ~11K tokens for 5 queries)concept-argocd-ghost-override— ArgoCD Image Updater ghost override patternincident-phase5-deployment-outage-2026-03-06— deployment outage root cause + resolutiondecision-phase6-vector-search-architecture— embedding model research, hardware analysis, Phase 6 architectural decisionsqa-phase7c-backfill-2026-03-07— QA report for Phase 7c backfill runconvention-block-first-access— block-first convention for agent knowledge access (91.1% token reduction)
Related
sop-secrets-management— secrets strategy for Phase 3 CNPG credentialstodo-pal-e-docs-deployment-reliability— the incident analysis that triggered thisplan-2026-03-01-pal-e-sprints— sprint tables need Postgres for safe migrationsservice-onboarding-sop— update when services start consuming shared Postgresphase-postgres-4a-barman-plugin-migration— Barman Cloud Plugin migration (before CNPG 1.29 upgrade)plan-2026-02-28-woodpecker-sdk-mcp— the SDK→MCP pattern Phase 8 follows