Skip to content

Commit c478052

Browse files
authored
docs: Postgres data model reference + stale schema doc cleanup (#2127)
* docs: Add Postgres data model reference and tidy stale schema docs Consolidates the database-per-service plus schema-per-tenant topology into a single reference covering service ownership, tenant isolation mechanism, reference data replication, and the one cross-tenant access pattern. Reflects PR #2125 (public removed from search_path) and PR #2126 (self-service signup). Updates ADR-0003 with a pointer to the new doc and marks the CockroachDB compatibility section as historical. Archives the completed database-per-service migration runbook. * docs: Update links after archiving database-per-service migration runbook * docs: Fix relative links in archived database-per-service runbook --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 08cdb8a commit c478052

18 files changed

Lines changed: 340 additions & 63 deletions

docs/adr/0002-microservices-per-bian-domain.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -667,12 +667,7 @@ GRANT ALL ON DATABASE meridian_current_account TO current_account_svc;
667667

668668
### Migration Strategy
669669

670-
See [Database-Per-Service Migration Runbook](../runbooks/database-per-service-migration.md) for:
671-
672-
- Step-by-step migration guide
673-
- Rollback procedures
674-
- Verification checklists
675-
- Lessons learned
670+
The initial database-per-service migration is complete. See the [archived migration runbook](../archive/database-per-service-migration.md) for historical context, or the [Data Model Reference](../architecture/data-model.md) for the current topology.
676671

677672
### Consequences
678673

@@ -693,4 +688,5 @@ See [Database-Per-Service Migration Runbook](../runbooks/database-per-service-mi
693688
### References
694689

695690
* [ADR-003: Database Schema Migrations](./0003-database-schema-migrations.md)
696-
* [Migration Runbook](../runbooks/database-per-service-migration.md)
691+
* [Data Model Reference](../architecture/data-model.md)
692+
* [Migration Runbook (archived)](../archive/database-per-service-migration.md)

docs/adr/0003-database-schema-migrations.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,9 @@ Database: meridian_current_account
155155

156156
- Each service has its own PostgreSQL database
157157
- Each organization gets its own schema within each service database
158-
- Connection URL includes `search_path` for org routing: `postgres://...?search_path=org_acme_bank`
158+
- The search_path is set transactionally via `SET LOCAL search_path TO org_<id>` by `shared/platform/db/gorm_tenant_scope.go`
159159
- Queries use unqualified table names; PostgreSQL resolves via `search_path`
160+
- As of PR #2125, `public` is **not** in the search_path — reference data is replicated into each tenant schema on provisioning rather than shared via public. See [data-model.md](../architecture/data-model.md#tenant-isolation-mechanism) for the current mechanism.
160161

161162
### Naming Conventions
162163

@@ -484,7 +485,9 @@ If migrating from golang-migrate:
484485
* Maintain migration history (no need to replay all migrations)
485486
* Continue using immutability principles
486487

487-
### CockroachDB Compatibility Considerations
488+
### CockroachDB Compatibility Considerations (Historical)
489+
490+
> **Note:** Meridian now targets PostgreSQL 16 exclusively. The constraints below are retained because existing migrations were authored under CockroachDB compatibility rules and continue to follow them (no PL/pgSQL, split column-add from partial-index-add, explicit timestamp columns instead of range types). New migrations should follow the same conventions for consistency, but the underlying runtime is Postgres. See [data-model.md](../architecture/data-model.md) for the current topology and [docs/reports/cockroachdb-migration-audit.md](../reports/cockroachdb-migration-audit.md) for historical context.
488491
489492
While CockroachDB is PostgreSQL-compatible, some PostgreSQL features are **not supported**:
490493

docs/architecture/data-model.md

Lines changed: 281 additions & 0 deletions
Large diffs are not rendered by default.

docs/runbooks/database-per-service-migration.md renamed to docs/archive/database-per-service-migration.md

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
---
22
name: database-per-service-migration
3-
description: Step-by-step guide for migrating from single shared database to database-per-service architecture
4-
triggers:
5-
6-
- Migrating to database-per-service architecture
7-
- Setting up new service databases
8-
- Database isolation for microservices
9-
- Multi-tenant database setup
3+
description: "[ARCHIVED] Historical runbook. The database-per-service migration is complete. See docs/architecture/data-model.md for the current topology."
4+
status: archived
5+
triggers: []
106

117
instructions: |
12-
Create separate database per service following naming convention meridian_<service>.
13-
Set up schema-per-tenant within each database. Configure dedicated database users
14-
with restricted grants. Update Atlas configuration per service. Use gRPC for
15-
cross-service data access instead of SQL joins.
8+
ARCHIVED. This runbook documented the one-time migration from a shared database
9+
to database-per-service. That work is complete - every service already owns its
10+
own Postgres database with schema-per-tenant isolation. For the current topology,
11+
see docs/architecture/data-model.md.
1612
---
1713

1814
# Database-Per-Service Migration Runbook
@@ -394,5 +390,5 @@ kubectl scale deployment current-account --replicas=3 -n production
394390

395391
- [ADR-002: Microservices Per BIAN Domain](../adr/0002-microservices-per-bian-domain.md)
396392
- [ADR-003: Database Schema Migrations](../adr/0003-database-schema-migrations.md)
397-
- [Incident Response Runbook](./incident-response.md)
398-
- [Disaster Recovery Runbook](./disaster-recovery.md)
393+
- [Incident Response Runbook](../runbooks/incident-response.md)
394+
- [Disaster Recovery Runbook](../runbooks/disaster-recovery.md)

docs/guides/new-bian-service-checklist.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ go build ./... # Verify generated code compiles
117117
Create the domain model with business rules and validation.
118118

119119
**Convention references**:
120+
120121
- [Error Conventions](error-conventions.md) — sentinel error naming and where to define them
121122
- [Repository Conventions](repository-conventions.md) — interface location, method naming, error documentation
122123
- [Value Types](value-types.md) — choosing between Money, Asset, and Amount for domain fields
@@ -186,6 +187,7 @@ go test ./services/{service}/domain/... -v
186187
Implement the repository using GORM.
187188

188189
**Convention references**:
190+
189191
- [Repository Conventions](repository-conventions.md) — entity-prefixed errors, TableName, optimistic locking, tenant scoping
190192
- [Value Types](value-types.md) — mapping Qty/Money/Amount to/from persistence columns
191193

@@ -357,7 +359,7 @@ func (EntityName) TableName() string {
357359
**Why singular**: Natural SQL syntax (`FROM account` not `FROM accounts`)
358360
**Why unqualified**: Allows `search_path` to route queries to tenant schemas
359361

360-
**Reference**: [Database-Per-Service Migration Runbook](../runbooks/database-per-service-migration.md)
362+
**Reference**: [Data Model Reference](../architecture/data-model.md)
361363

362364
**Verification:**
363365

@@ -1784,4 +1786,4 @@ task-master parse-prd docs/guides/new-{service}-service-checklist.md
17841786
- [Adding Starlark Service Bindings](adding-starlark-service-bindings.md)
17851787
- [Circuit Breaker Usage Guide](circuit-breaker-usage.md)
17861788
- [Testcontainers Usage Guide](testcontainers-usage.md)
1787-
- [Database-Per-Service Migration Runbook](../runbooks/database-per-service-migration.md)
1789+
- [Data Model Reference](../architecture/data-model.md)

docs/guides/repository-conventions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ func EncodePartyCursor(c PartyCursor) string {
293293
```
294294

295295
Use cursor pagination when:
296+
296297
- The dataset is large (thousands of rows)
297298
- Stable ordering is required under concurrent inserts
298299
- The caller needs to resume from a previous position

docs/prd/046-economy-visualization-completeness.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ both need updating:
101101
from the manifest. Currently shows 4 types in Resources tab.
102102

103103
These serve complementary roles:
104+
104105
- **Overview** = visual map + high-level summary (graph-first)
105106
- **Explorer** = detailed browse by resource type (list-first)
106107

@@ -225,6 +226,7 @@ extraction paths:
225226
- `party_type` — from `manifest.partyTypes[]`
226227

227228
Edge types:
229+
228230
- `sourced_by` — market_data_set → market_data_source
229231
- `typed_as` — internal_account → account_type
230232
- `denominated_in` — internal_account → instrument
@@ -247,6 +249,7 @@ Add node components following the existing `memo()` pattern
247249
(`InstrumentNode`, `AccountTypeNode`, etc.).
248250

249251
**ELK layer priorities** (lower = higher in graph):
252+
250253
- Instruments: 50
251254
- Market Data Sources: 45
252255
- Market Data Sets: 40
@@ -268,6 +271,7 @@ types, market data) hidden by default — toggled on via filter.
268271

269272
**Grouped filter panel**: Group toggles into 6 categories
270273
instead of 13 individual checkboxes:
274+
271275
- Financial Core (instruments, account types, valuation rules)
272276
- Workflows (sagas)
273277
- Market Data (sources, sets)
@@ -295,6 +299,7 @@ to the Explorer page for the full breakdown.
295299
#### 2f. Double-click navigation
296300

297301
Add navigation targets for all new types:
302+
298303
- market_data_source/set → `/market-data`
299304
- organization → `/parties`
300305
- internal_account → `/internal-accounts`
@@ -370,6 +375,7 @@ type can follow in later PRDs.
370375
`ReconcileManifest` output. Drift items as a DataTable:
371376
resource_type, code, drift_type (MISSING, MODIFIED, EXTRA),
372377
description. "Run Reconciliation" button with:
378+
373379
- Loading state during RPC (can take 5-30 seconds)
374380
- Warning display when services are unreachable
375381
- "No manifest applied" message for pre-045 tenants

docs/prd/047-security-audit-status.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ limits, allowing any single container to exhaust host resources.
5050
### PR #1712 — Enforce Saga Step Limit + Remove Dead MemoryWarningThreshold
5151

5252
**Findings**:
53+
5354
- HIGH-1 — `MaxStepsPerExecution = 1_000_000` was defined but `SetMaxExecutionSteps()`
5455
was never called on the saga runtime thread, allowing CPU exhaustion via tenant scripts.
5556
- HIGH-4 (partial) — `MemoryWarningThreshold` constant was defined but never referenced
@@ -157,6 +158,7 @@ are Wave 2 work (approximately 1 week)
157158
### PR #1736 — Add OPA Gatekeeper Constraint + Security Defaults CI Gate
158159

159160
**Findings**:
161+
160162
- HIGH-3 (regression prevention) — OPA Gatekeeper had no policy blocking
161163
`AUTH_ENABLED: "false"` in ConfigMaps.
162164
- HIGH-3 (CI gate) — No CI job verified security defaults after PRs.

docs/prd/052-email-platform.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ type TemplateRenderer interface {
174174
```
175175

176176
**Three Sender implementations**:
177+
177178
- `resend.Sender` - production, wraps Resend Go SDK with circuit breaker
178179
- `log.Sender` - writes to application log, no API call (for `EMAIL_MODE=log`)
179180
- `noop.Sender` - discards silently (for `EMAIL_MODE=disabled` and tests)
@@ -235,6 +236,7 @@ Four templates using Go `html/template` with `embed.FS`, stored in
235236
| `account-frozen.html` | Account frozen (dunning level 3) | account ID, frozen reason, support contact |
236237

237238
**Design principles**:
239+
238240
- Single dunning template with severity parameter (reduces duplication,
239241
ensures brand consistency across escalation levels)
240242
- Responsive HTML (single-column, mobile-first)
@@ -361,6 +363,7 @@ Adapted from `shared/platform/events/metrics.go` (rename subsystem):
361363
| `email_circuit_breaker_state` | Gauge | 0=closed, 1=half-open, 2=open |
362364

363365
**Alerting rules** (Prometheus/Alertmanager):
366+
364367
- `email_outbox_pending_total > 100` for 5 minutes -> warn (backlog)
365368
- `email_outbox_dead_letter_total` increase > 0 -> alert (permanent failure)
366369
- `email_circuit_breaker_state == 2` for 5 minutes -> alert (Resend down)
@@ -437,6 +440,7 @@ payment-order service.
437440
### Task 2: Email service package (3 points)
438441

439442
`shared/pkg/email/`:
443+
440444
- `sender.go` - `Sender` interface, `Message` type
441445
- `template.go` - `TemplateRenderer` using `html/template` + `embed.FS`
442446
- `outbox.go` - Outbox repository (write, read pending, update status,
@@ -449,6 +453,7 @@ payment-order service.
449453
- `noop/provider.go` - No-op sender for tests
450454

451455
**Reuse explicitly**:
456+
452457
- `circuitbreaker.go` wrapping Resend client
453458
- `shared/pkg/tokens/` for any future token operations
454459
- Go `html/template` only (lint rule: no `text/template` in email package)
@@ -457,6 +462,7 @@ payment-order service.
457462

458463
Implement `dispatch.DispatchableInstruction` for `EmailOutboxRow`. Write a
459464
`BatchProcessor` callback that:
465+
460466
1. Renders template via `TemplateRenderer`
461467
2. For dunning emails: checks invoice status, cancels if PAID
462468
3. Calls `Sender.Send()` with idempotency key as Resend header
@@ -481,6 +487,7 @@ as the event worker. Add circuit breaker state gauge. Mechanical adaptation.
481487
### Task 5: Resend webhook endpoint (2 points)
482488

483489
`POST /api/v1/webhooks/resend`:
490+
484491
- Verify `Svix-Signature` header (Resend webhook signing)
485492
- Parse event type: `email.delivered`, `email.bounced`, `email.complained`
486493
- Look up audit log entry by `provider_id`
@@ -493,6 +500,7 @@ is the auth mechanism).
493500
### Task 6: Templates (3 points)
494501

495502
Four templates in `shared/pkg/email/templates/`:
503+
496504
- `invoice.html` + `invoice.txt`
497505
- `dunning-notice.html` + `dunning-notice.txt` (parameterized: severity
498506
1/2/3 with `{{if eq .Severity 1}}` blocks)
@@ -566,6 +574,7 @@ EMAIL_BASE_URL=https://meridianhub.cloud
566574
```
567575

568576
**EMAIL_MODE**:
577+
569578
- `disabled` - No outbox writes, no sending. For unit tests.
570579
- `log` - Writes outbox + audit log, renders templates, but does not call
571580
Resend API. For integration tests and CI.

docs/prd/053-auth-email-flows.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ concern bridging the party service to authentication (see PRD 031).
108108
### Task 2: Email verification flow (3 points)
109109

110110
**Backend**:
111+
111112
- Migration: `email_verification_tokens` table (token_hash, identity_id,
112113
expires_at, consumed_at)
113114
- On registration (when verification required): generate token via
@@ -119,6 +120,7 @@ concern bridging the party service to authentication (see PRD 031).
119120
(3/hour per identity via DB count query on outbox)
120121

121122
**Frontend**:
123+
122124
- Post-registration "check your email" page with resend button
123125
- Verification landing page (success, expired, already-verified, invalid
124126
states)
@@ -127,6 +129,7 @@ concern bridging the party service to authentication (see PRD 031).
127129
### Task 3: Password reset flow (3 points)
128130

129131
**Backend**:
132+
130133
- Migration: `password_reset_tokens` table (token_hash, identity_id,
131134
expires_at, consumed_at)
132135
- `POST /api/v1/forgot-password` - always returns 200 (timing-safe:
@@ -142,6 +145,7 @@ concern bridging the party service to authentication (see PRD 031).
142145
INTERVAL '1 hour'`
143146

144147
**Frontend**:
148+
145149
- "Forgot password?" link on login page
146150
- Email input form -> "check your email" confirmation
147151
- Reset form (new password + confirm) -> success -> redirect to login

0 commit comments

Comments
 (0)