Repository navigation
AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships (P27-S01) #1998
Description
AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships
Approved implementation ticket. Plan key: P27-S01. Labels: type:feature, complexity:hard. Blocking prerequisites are tracked as native "blocked by" links on this issue (currently #570, #606, #1947 and #2250), not as a label. Publication does not authorize implementation before its documented gates are satisfied.
Parent
Phase 9 owns this enabling source; Phase 27 consumes it through the explicit blockers below. #1992 is a related consumer specification, not a second native parent.
Source of truth
Documentation PR: #1993. Exact specification commit: b5a65b3f2da880fcbfa4b3cae189d216d06542be. Inspected implementation baseline: 71b81fae2ce1f9b615bcfee8606cd2c978a70e71. Publishing this contract does not establish implementation, merge, qualification or activation.
- docs/prds/sitestacker-parity/phase-09-full-crm-depth-relationship-graph.md — Dated Phase 22 D19 Ministry Assignment source-ownership amendment (2026-08-06)
- docs/adr/0136-organization-owned-ministry-assignments-and-separated-support-access.md — Decision and database enforcement
- docs/prds/sitestacker-parity/phase-27-donor-development/partnerships.md
- Existing Party-edge producer; not this source.
- Existing consumer explicitly blocked on this Phase9 contract.
Stories: US27-P02. Primary acceptance ownership: Already accepted Phase 9/ADR0136 enabling contract; source-owned criteria below. No invented Phase 27 acceptance ID..
Applicable ADRs:
- docs/adr/0136-organization-owned-ministry-assignments-and-separated-support-access.md
- docs/adr/0001-asym-postgres-owns-crm-truth-twenty-retired.md
What to build
Implement the Phase9-owned Ministry Assignment identity/current eligibility and versioned lifecycle plus zero-to-many prospective Party Participant Memberships. Build a narrow accessible CRM People/association context to create/select the identified ministry and add/end/correct exact participant intervals. The record owns exactly one Tenant and Legal Entity; a Party is same Tenant without invented Party Legal Entity ownership. Identity survives participant/spouse/team/login changes. Membership is append-only-corrected with exact non-overlapping half-open intervals, trusted attribution/current-head command, scope-complete uniqueness/idempotency and deletion restriction. Preserve owner-certified source correction/lifecycle evidence for consumers. Expose narrow authorized current identity, lifecycle/eligibility and permitted participants, not raw membership rows. Include migration census with explicit unresolved legacy profile mappings, no guessed spouse/team association. Deliver browser/API/persistence behavior with real current-authority and interval concurrency proof.
Context and contracts
ADR0136 and the dated Phase22 D19 amendment explicitly place this source in Phase9. Existing #607 owns Party-to-Party relationship edges; #1285 owns Page contributors/display and explicitly lists Ministry Assignment as an external Phase9 prerequisite. Exact phrase issue search found no implementation producer. This is a missing already-decided producer, not a new P27 entity or a Ministry Project/Support Assignment merge.
Current authority, tenant/field/history-safe read and write behavior, atomic source history/receipt/required intent, truthful lost-response recovery and applicable two-session concurrency proof accompany this slice. Base UI/base-maia/Zinc, keyboard/screen-reader/mobile/zoom behavior and slow/failing network recovery accompany every reached screen. These are in-slice delivery obligations under REQ27-Q01–Q14, not deferred to final qualification. Terminal Q tickets prove composition and release evidence.
Scope and non-goals
- No Public Ministry Page, Page contributor/display grant, Support Assignment or binding writer, finance or notification access, invitation/shared login, new authorization engine or person-owned fund.
- No automatic participant propagation, current-work assignment, donor enrollment, source history union or destructive merge/split.
- No requirement to finish all Phase9; no arbitrary new membership roles/lifecycle states beyond accepted source contract.
Acceptance criteria
- A ministry with zero current participants exists and is selectable under current permitted exact Tenant/Legal Entity scope without a Page, gift, email, support binding or login.
- One Party may participate in several ministries, several Parties may participate in one ministry, and changing spouse/team/leadership/login names does not replace ministry identity.
- Prospective add/end/correct records exact half-open membership interval and immutable source evidence; overlapping concurrent membership intervals in one exact scope cannot both commit, adjacent intervals behave correctly.
- Current principal/field/history/purpose authority applies before list/count/detail/result and at acceptance; real cross-Tenant/entity, caller-supplied actor/scope, raw grants/RLS/RPC and privileged-service attacks cannot add forbidden facts or disclose restricted participants.
- Membership alone creates no responsibility, task, notification, Page display/edit, Support Workspace/supporter identity or finance grant. Former member with independent current grant and member without grant receive the correct independent policy outcome.
- Retirement or owner-certified identity correction preserves source identity/history and exposes current eligibility without retargeting existing released or accepted downstream facts; actual source changes can be consumed prospectively.
- Lost response/retry/ABA/revocation race leaves one intended mutation and original actor/time; before-commit failures leave no partial source/history/receipt.
- Evidence-based migration and rollback preserve restrictions and history; ambiguous profile-to-many-ministry cases remain unresolved, no inferred membership or money/attention effect.
- Keyboard/screen-reader/single-pointer/320px/400% zoom/RTL/international-name workflows allow staff to distinguish Associated with ministry from all independently granted rights; restricted alias never falls back to legal name/location.
Primary source cases are reproduced below so the implementer can distinguish one slice's first-demonstration ownership from later composed regression proof. An AC whose multi-step scenario crosses a later producer must have that producer as a genuine blocker or its primary acceptance transferred explicitly; a mocked internal owner does not satisfy it.
Testing and verification
Ship the behavior with substantive TDD, actual authenticated Mission Control browser → API → disposable Supabase persistence evidence, effective-role grant/RLS/RPC negatives and two independently controlled PostgreSQL sessions for each named race; also fault before/after commit, lost-response reconciliation, same-key changed-meaning refusal, current-authority replay disclosure and keyboard/screen-reader/320 CSS px/400% zoom/single-pointer proof. Existing mocked task/CRM and demo-auth suites are regression seams only. Record the exact new focused test files and runnable commands in the implementation PR; no invented test:p27 command is claimed here. Add concurrent half-open interval and retirement/current-reference fixtures. Use actual non-owner database roles and independently configured current authorization; service-role setup alone is not proof.
Verified existing commands (future implementation checks, not a claim that this new behavior is already tested):
bunx vitest run tests/unit/packages/api/admin/mission-control-tasks.test.ts tests/unit/packages/api/crm-auth.test.tsbun run verify:supabase-migrationsbun run verify:data-boundarybun run test:e2e:strictbun run test:a11y:strictbun run ci:preflight
Commands verified in current develop package manifest/test paths but NOT executed for this research task.; verify:supabase-migrations uses disposable localhost PostgreSQL compatibility bootstrap, not alone a real Supabase auth/Data API/RLS gate.; test:e2e:strict still needs genuinely authenticated non-demo disposable backend configuration; default Playwright environments include auth bypass.; Unit tests are in-memory mocks; SQL migration test is static inspection, neither qualifies database authorization or atomicity.
Completion evidence must link the implementation commit and exact focused test commands/results to the listed story/REQ/AC IDs; show before/after user or source outcomes, real-role denial evidence and each relevant race/recovery result. Extend existing stable seams using red-green-refactor; no production test endpoint, alternate task engine or copied in-memory owner substitutes.
Operational controls
Honor reached source controls; this slice introduces no additional monitor threshold. Final qualification does not postpone source repair or safe failure behavior.
Blocked by
- [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606 — [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606: Canonical Tenant-scoped Party spine and source identity/lifecycle/tombstone references Why: Membership must point to a real Party; no profile/person shadow subject.
Non-issue prerequisites
- Integrate docs(AL-1992): publish Phase 27 specification and implementation handoff #1993 governing specification into the implementation target before dispatch. This PR merged on Oct 11, 2026, so this prerequisite is satisfied. Do not mark this ticket ready because its body is complete.
- Documentation PR exact immutable baseline and existing accepted ADR0136/P9 dated owner amendment govern; propose under [Phase 9] Epic — Full CRM Depth & Relationship Graph #604, related/blocking AL-1992 — Phase 27 Donor Development & Portfolio Management specification #1992 only through approved native dependency relationships.
- No dependency on [Phase 22] P22-03 — Create one exact typed Public Ministry Page #1284/[Phase 22] P22-04 — Manage distinct Ministry Assignment, display, and contributor grants #1285 public Page creation or any entire Phase22/Phase21 release.
- Phase12 sole current policy decision point: Trusted current principal, target/field/purpose/history policy and current authorization epoch at read/write/result boundary. Existing source components: [Phase 12] Capability spine core: branded Capability + runtime-verifiable EffectiveAccess + one resolver (PDP) + retire the incumbent + names-never-authorize gates + DRT reference model + D22 non-prod isolation #667, [Phase 12] Tenant-axis substrate: tenant-in-the-brand + unified RLS source + delete OR is_super_admin() + positive RLS-isolation gate + kill defaults + cross-tenant-edge invariants #668, [Phase 12] Principal foundation: four principal classes + active-assignment primitive + fire-time re-resolution + profiles.role inert-for-authz #669, [Phase 12] Revocation + grant-state: single monotonic tenant epoch + governance_version cache + assignment-bound grant contract + advisory-locked grant-state function + last-admin/last-clearance guards #670, [Phase 12] Module / entity / field permissions (CRUD, per-field on the Phase-11 catalog, edit-implies-view lattice, manager-rollup opt-in + fail-closed cycle, draft/unpublished, self-scope, merge/dedupe re-derivation) #675, [Phase 12] Classification / security tags / geo-precision / cover-identity alias / protected-constituent floor row / restricted-non-exportable #676, [Phase 12] Purpose/consent axis (resolve with purpose bound to surface + consent AND-floor + contactability) + residency axis (region veto + placement build-invariant + no-default-for-restricted) #680. Integrate and qualify the exact consumed principal/Tenant/target/field/history/purpose/revocation components before enabling this path; no whole-phase or unrelated admin-UI prerequisite. Existing CRM role bridge is not a substitute.
- contract: Trusted current principal, target/field/purpose/history policy and current authorization epoch at read/write/result boundary; owner: Phase12 sole current policy decision point; existing_issues: [667, 668, 669, 670, 675, 676, 680]; gate: Integrate and qualify the exact consumed principal/Tenant/target/field/history/purpose/revocation components before enabling this path; no whole-phase or unrelated admin-UI prerequisite. Existing CRM role bridge is not a substitute.
- Recheck current source component revisions and qualification at implementation start. Existing issue closure alone is not a runtime certificate. Missing qualified inputs gate only the consumed lane, never authorize a substitute source or broader permissions.
Implementation constraints
- Use current Bun/Turborepo/Next.js, shared Base UI/base-maia and approved TanStack data boundaries; no new component library or feature-local Supabase.
- Source state, attributable history, effect receipt and required local work/owned exception commit together; external owners remain independent. Current authority governs result/replay disclosure.
- Server derives actor and scope. Qualify old and resulting row, all changed fields, grants/RLS USING and WITH CHECK semantics, views/RPC/service/import/repair paths. Assignment, membership and stage grant nothing.
- No automatic donor outreach, lifecycle transition, supporter assent, finance mutation, public publication, missionary quota or compulsory encounter logging.
- Security, atomicity, source ownership, accessible UX, migrations/old-writer fencing and the applicable Q01–Q06/Q09–Q11/Q13/Q14 proof travel inside this behavior; terminal release qualification is not a substitute.
- Document merge gate: use root documentation PR exact immutable commit before implementation; approval of specification does not approve this ticket breakdown. Parent AL-1992 — Phase 27 Donor Development & Portfolio Management specification #1992 remains unchanged.
Read AGENTS.md, the nearest scoped instructions, testing rules, document authority and OpenSpec rules. The explicitly invoked to-tickets skill governs this ticket; do not invoke implementation as part of ticket publication. For implementation, inspect current manifests and installed framework guidance rather than freezing this baseline's versions into new code.
Fresh-context scope: assigned_acceptance_text_tokens_approx: 0; estimated_total_input_tokens: 11500; basis: Exact currently assigned full-primary AC text chars/4 plus 11,500 for requirement paragraphs, scoped instructions, focused actual source implementations and tests. Shared producer obligations are linked explicitly; do not load every predecessor volume. This is an estimate, not a tokenizer measurement.. Read focused source and tests for this behavior; retained historical review records are provenance, not mandatory bulk context. If a consumed source is missing, report its exact contract and keep the affected behavior gated; do not quietly expand the ticket to implement a whole earlier phase.
Delivery boundary
This ticket proves its behavior and mapped acceptance on its qualified inputs. It does not close the parent, mark unrelated requirements complete, satisfy another owner's unresolved decision, activate an unqualified cohort, or authorize merging the documentation PR. E7/C-01 remain explicit where reached. All retained implementation task boxes stay unchecked during planning.
Activity
[-]P27-S01: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships[/-][+]AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships (P27-S01)[/+]II-ricky-bobby-II commentedon Oct 24, 2026
Final spec r1 (Oct 24, 2026)
Status: pending Ready, prep only. Building starts only after Blake marks the issue Ready. This revision changes no labels and adds no Ready.
Scope: this spec covers the Phase 9 database source for one stable Ministry Assignment and its separate Party Participant Memberships (ADR-0136, Phase 9 dated D19 amendment). As filed, the issue spans five things: source tables, write commands, the server API with Phase 12 PDP integration, the People & access UI, and a legacy census. That is well over the size caps (§10). OQ1 proposes a split. By default, #1998 keeps S1: the source schema, the append-only history, the guarded command and narrow read functions, the read-only legacy census, and real-database race and grant proof. The browser and API surfaces move to follow-ups that stay gated on the Phase 12 runtime.
anonorauthenticated. It has no app caller and no UI, and it adds nothing to the [Phase 4] #505 slice 4/4: CI gates: RLS catalog assertion + bare service-role client gate #2253 allowlist.legal_entities([Phase 7] Canonical Legal Entity and immutable issuer-profile runtime foundation (Backlog draft) #1947) aren't on develop (§11).§0 What I checked (Oct 24, 2026, about 4:35 PM PT)
6ac594d9(Oct 24, 2:34 PM PT), after a full fetch of 41 refs. The newest migration is20261024120000. There are 83 versioned files, plus 5rollback_*scripts with no version.complexity:hard,type:feature. The body still saysstatus:blocked, but that label isn't applied.b5a65b3fis in history.docs/adr/0136-organization-owned-ministry-assignments-and-separated-support-access.md§Decision, "Database enforcement" paragraph: complete Tenant + Legal Entity scope, composite FKs, non-overlapping half-open intervals, scope-complete uniqueness and idempotency, deletion restriction, forced coarse Tenant RLS, noanon/authenticatedgrants on raw relations, and definer functions outside exposed schemas with a fixedsearch_pathandPUBLIC EXECUTErevoked.docs/prds/sitestacker-parity/phase-09-full-crm-depth-relationship-graph.md:1299-1341: the D19 amendment. It describes a membership as "prospective, effective-dated, append-only-corrected", and says the Ministry Assignment is distinct from Party, household, relationship edge, Ministry Project, Support Assignment and Page.parties,persons,householdsorlegal_entitiestable, and no column namedparty_idorlegal_entity_idanywhere.missionarieshas noperson_idcolumn. Its columns areid, tenant_id, profile_id, bio, …, birth_date.plpgsql,pgcrypto,uuid-ossp. Nobtree_gist. No migration usesEXCLUDEortstzrangeyet.authzhasmembershipspluscurrent_tenant_id()(JWTapp_metadata.tenant_id),current_user_id(),has_membership,has_staff_membershipandis_super_admin(). No PDP, epoch, grant table or principal exists on develop.public.support_assignments(20260515025814_support_hub_core_modules.sql:448) is the Support Hub conversation assignment. It is not Phase 21's Support Assignment (ADR-0108). S1 references neither.packages/api/src/crm/auth/access.ts:22-62:requireCrmAccessis a role bridge (staff/admin/super_admin). The issue says this "is not a substitute" for Phase 12.packages/api/src/admin/crm/relationships/service.ts:51-61readsdonors/missionaries/profilesthrough theRelationshipsClient.apps/admin/app/(app)/crm/detail-drawer.tsx(694 lines) is the CRM People surface that S3 would extend.20261024170500_party_spine,20261024171500_party_spine_tenant_guard):partiesPK(tenant_id, id),party_kind ∈ {person, household, org}.20261024180000_party_spine_lifecycle_tags_tombstone,20261024181500_drop_donor_free_text_relics):parties.merged_into_party_idandlifecycle_status.builder/2250-tenant-guard,20261024150000_tenant_guard.sql): thetenant_guardNOLOGIN NOBYPASSRLS role,tenant_guard.current_tenant_id()=nullif(current_setting('app.current_tenant_id', true), '')::uuid, guarded definer functions owned bytenant_guard,TO tenant_guardpolicies, and a requiredtenant_idecho.legal_entities.§1 Intended behavior
A Ministry Assignment is an organization-owned row with exactly one Tenant and one Legal Entity. Its identity never changes. Its name and lifecycle change only by appending a version, under a compare-and-swap on the head version.
A membership links one same-Tenant
personParty to one assignment for one half-open interval[valid_from, valid_to). For one (Tenant, assignment, Party):Ending or correcting a membership appends a successor row and marks the old one superseded. Nothing is deleted.
Every command:
(tenant_id, idempotency_key);anonreads raw rowsPUBLIC/anon/authenticated/service_role; no grants; RLS ENABLE+FORCETO tenant_guardpolicies keyed ontenant_guard.current_tenant_id(); composite(tenant_id, …)FKs;tenant_idecho(tenant_id, party_id)→parties; command refusesmerged_into_party_id IS NOT NULLorparty_kind <> 'person'(tenant_id, legal_entity_id, ministry_assignment_id)→ assignment'sUNIQUE (tenant_id, legal_entity_id, id)EXCLUDE USING gistover in-effect rows (needsbtree_gist, OQ3)ON DELETE RESTRICTNon-goals: Public Ministry Page, Support Binding Version, Phase 21 Support Assignment, Page contributors or display, notifications, finance, invitations, the PDP itself, Realtime, views,
requireCrmAccesschanges, inferred memberships, and writes tomissionaries.person_id.§2 In (S1)
20261024185000_ministry_assignments_source.sql(provisional version, §11), containing:CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA extensions(OQ3).public.ministry_assignments:tenant_id,id,legal_entity_id,head_version int NOT NULL DEFAULT 1,created_by_user_id,created_at;(tenant_id, id)andUNIQUE (tenant_id, legal_entity_id, id);(tenant_id, legal_entity_id)→legal_entitiesON DELETE RESTRICT.public.ministry_assignment_versions(append-only):display_name,lifecycle_statusCHECK('active','retired'),change_kindCHECK('create','revise','retire','owner_certified_correction'),reason,actor_user_id,recorded_at;(tenant_id, ministry_assignment_id, version);public.ministry_assignment_memberships:party_id,valid_from,valid_to(NULL = open),change_kindCHECK('add','end','correct'),supersedes_membership_id,superseded_by_membership_id,reason,actor_user_id,recorded_at;valid_to IS NULL OR valid_to > valid_from;EXCLUDE USING gist (tenant_id WITH =, ministry_assignment_id WITH =, party_id WITH =, tstzrange(valid_from, valid_to, '[)') WITH &&) WHERE (superseded_by_membership_id IS NULL).public.ministry_assignment_command_receipts: PK(tenant_id, idempotency_key), pluscommand,request_hash,result jsonb,actor_user_idandrecorded_at.tenant_guard, following [Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250's pattern (ownertenant_guard,SET search_path = '', every name schema-qualified,REVOKE EXECUTE … FROM PUBLIC, anon, authenticated,GRANT EXECUTE … TO service_role, each sets the GUC fromp_tenant_idand echoestenant_id):ministry_assignment_createministry_assignment_revise(CAS onp_expected_version; also covers retire and correction)ministry_membership_addministry_membership_endministry_membership_correctministry_assignment_current: the narrow read. It returns id, legal entity, current name, lifecycle,eligible = (lifecycle_status = 'active'), head version, and the party ids currently in effect. It never returns raw membership rows.valid_from/valid_to≥statement_timestamp());correctmay change a past interval, and it needs a non-emptyreason(OQ5);retiredassignment (OQ6)."<table> tenant guard isolation" FOR ALL TO tenant_guard USING/WITH CHECK (tenant_id = tenant_guard.current_tenant_id()).REVOKE ALL … FROM PUBLIC, anon, authenticated, service_role. Thentenant_guardgets onlySELECT, INSERTon all four tables, plusUPDATE (superseded_by_membership_id)on memberships andUPDATE (head_version)on assignments.scripts/verify/ministry-assignment-census.mjs:missionaries (tenant_id, id, profile_id), never*and neverperson_id.docs/features/crm-ministry-assignments/README.md: the model, the commands, "membership grants nothing", and how to run the census.§3 Out (S1)
withTenantallow-list entries.docs/ci.md, the [Phase 4] #505 slice 4/4: CI gates: RLS catalog assertion + bare service-role client gate #2253 allowlist,authz.*,requireCrmAccess, ormissionaries.§4 Done criteria
(A) Acceptance, real stack (
tests/unit/acceptance/issue-1998/ministry-assignments.realstack.acceptance.test.ts; two independentpgclients; fixtures are 2 tenants, 2 legal entities, and persons, a household and a tombstoned Party):ministry_assignment_currentreturns it as eligible. It needs no Page, gift, email, binding or login.id.[t1,t2)then[t2,t3)both commit (adjacent).23P01.endandcorrectappend rows; the predecessor'ssuperseded_byis set; row count only grows.party_id;p_tenant_idvs. a row id;householdor tombstoned Party;anon/authenticatedSELECT/INSERT/EXECUTE(42501);service_roledirect tableSELECT(42501);expected_version. One wins; the other is refused; there is no lost update. An ABA attempt with a stale version is refused.eligible=false; history and memberships are kept; a new add is refused; end and correct still work.ministry_assignment_memberships, and noauthzobject reads it.(B) Unit and static (
tests/unit/scripts/ministry-assignment-census.test.tsandtests/unit/supabase/ministry-assignments-migration.test.ts):PUBLIC EXECUTErevoked on all six functions,search_path = '', and nois_super_admin,authz.current_tenant_idorperson_idtokens.(S) Security
btree_gistavailability matches OQ3. S1 drops and recreates no existing policy. Any mismatch goes to Samson before merge.(Handback) The PR body lists the test files, the commands and their results, the s1 result, the census output on the seeded DB, and
Closes #1998only if OQ1 is approved. Otherwise it usesPart of #1998.§5 Mapping (issue AC → S1 criteria)
§6 Declared edits
supabase/migrations/20261024185000_ministry_assignments_source.sqlscripts/verify/ministry-assignment-census.mjsdocs/features/crm-ministry-assignments/README.mdtests/unit/acceptance/issue-1998/ministry-assignments.realstack.acceptance.test.tstests/unit/scripts/ministry-assignment-census.test.tstests/unit/supabase/ministry-assignments-migration.test.ts§7 Pins
parties (tenant_id, id). Tombstone: [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606'smerged_into_party_id. Legal Entity: [Phase 7] Canonical Legal Entity and immutable issuer-profile runtime foundation (Backlog draft) #1947'slegal_entities (tenant_id, id). If any of these lands with a different shape, stop and escalate to Samson. Don't build substitutes.authz.current_tenant_id(only [Phase 12] #668 slice (f): switch authz.current_tenant_id() to the request tenant; regression set; non-divergence; docs; Closes #668 #2298 may). Don't putis_super_admin()in any policy ([Phase 12] #668 slice (d2): RLS rewrite on spine tables; is_super_admin out of every policy #2296).support_assignments, and don't reference the Support Hub table.§8 CI
No new step and no workflow edit.
ci-integration.yml"Apply migrations" (migrate,test-e2e-smoke,test-e2e) throughsupabase-migrations.mjs, with [Phase 4] #505 slice 4/4: CI gates: RLS catalog assertion + bare service-role client gate #2253's gate when present.ci.yml:test-unit"Run unit tests";format(bun run format:check),integrity,lint"Run affected lint on pull requests",typecheck"Run affected typecheck on pull requests" andacceptance-tests"Run acceptance tests" run as usual.bun x vitest run realstack.acceptance,TENANT_GUARD_IT_ADMIN_DATABASE_URL, fail-not-skip underGITHUB_JOB=migrate). That step isn't on develop yet; [Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250 is a prerequisite (§11).§9 Data, grants and
missionaries.person_idPUBLIC,anon,authenticated,service_role) on all four tables first. Then grant onlytenant_guardthe minimal privileges (§2).service_rolegets EXECUTE on the six functions only.missionaries.person_id. The census readsmissionaries(tenant_id, id, profile_id)only, and memberships referencepartiesdirectly. So S1 isn't a "first writer", and no native [Security] missionaries.person_id column-grant fix: anon cross-tenant read + select("*") routes (follow-up from #2256) #2268 block is needed. ([Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606, a prerequisite here, is already natively blocked by [Security] missionaries.person_id column-grant fix: anon cross-tenant read + select("*") routes (follow-up from #2256) #2268.)authz.*.TO tenant_guardguard policies, which [Phase 12] Tenant-axis substrate: tenant-in-the-brand + unified RLS source + delete OR is_super_admin() + positive RLS-isolation gate + kill defaults + cross-tenant-edge invariants #668/[Phase 12] #668 slice (f): switch authz.current_tenant_id() to the request tenant; regression set; non-divergence; docs; Closes #668 #2298 later reconciles.§10 Size
§11 Dependencies and migration
legal_entities), [Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250 (tenant_guardand the real-stack step). For S2 and S3 only: [Phase 12] Capability spine core: branded Capability + runtime-verifiable EffectiveAccess + one resolver (PDP) + retire the incumbent + names-never-authorize gates + DRT reference model + D22 non-prod isolation #667/[Phase 12] Tenant-axis substrate: tenant-in-the-brand + unified RLS source + delete OR is_super_admin() + positive RLS-isolation gate + kill defaults + cross-tenant-edge invariants #668/[Phase 12] Principal foundation: four principal classes + active-assignment primitive + fire-time re-resolution + profiles.role inert-for-authz #669/[Phase 12] Revocation + grant-state: single monotonic tenant epoch + governance_version cache + assignment-bound grant contract + advisory-locked grant-state function + last-admin/last-clearance guards #670/[Phase 12] Module / entity / field permissions (CRUD, per-field on the Phase-11 catalog, edit-implies-view lattice, manager-rollup opt-in + fail-closed cycle, draft/unpublished, self-scope, merge/dedupe re-derivation) #675/[Phase 12] Classification / security tags / geo-precision / cover-identity alias / protected-constituent floor row / restricted-non-exportable #676/[Phase 12] Purpose/consent axis (resolve with purpose bound to surface + consent AND-floor + contactability) + residency axis (region veto + placement build-invariant + no-default-for-restricted) #680. I added no links (OQ2).20261024185000is provisional.120000, and there's nothing in20261024[15-19]*.builder/2250-tenant-guardhas150000.140000/141000); Modernize Sentry, content, email and browser payment SDKs #2214 has no migrations.20261024164000–169999. In18xxxx, the only hits are [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606's180000/181500(also cited in [Phase 9] #606 slice a/3: parties lifecycle_status, tags, same-kind merge tombstone; spine verification; seed regeneration #2278, [Phase 9] #606 slice b/3: drop donors.type, organization, spouse; replace donors_assign_party; census v5; minimal excision #2279 and [Phase 9] #606 slice c/3 (deferred): drop donors.missionary_id, donors.notes, donor_activities after #608/#616/#617 #2280).185000appears nowhere.181500and [Phase 7] Party spine: parties supertype + persons anchor + households + household_members + org subtype + org_contacts + donors.party_id #570's170500/171500, and before reserved190000.185000, or develop moves past185000before the build, the Builder re-picks a free version newer than every prerequisite and re-runs this check (OQ8).§12 OQs (defaults in bold)
btree_gist. Default: enable it inextensionsand useEXCLUDE. That's race-proof without app locking. Alternative: an advisory-lock-per-key trigger, which is weaker and harder to prove.personParties only (ADR-0136: every spouse or teammate keeps a separate Party). Alternative: alloworg.correctmay change past intervals with a required reason.20261024185000provisionally; re-pick at build if any prerequisite sorts later.actor_user_id uuid(auth.usersid) until [Phase 12] Principal foundation: four principal classes + active-assignment primitive + fire-time re-resolution + profiles.role inert-for-authz #669 principals exist. S2 maps principal → user.donors/profiles, with no special handling. S1 adds noanonsurface.§13 Deploy
Assume production is behind. Production is missing all 58 develop versions from
20260515025814to20261024120000(25 applied, newest20260513173938), and the anon P0 ondonors/profilesis under Blake's containment decision. Dev is 55 behind, with four20261001*collisions and an extra20260519233845. develop's catalog is live nowhere hosted.Samson relays to Blake at merge. The factory runs nothing hosted.
schema_migrationsholds every develop version on the deploy base below this file's version, newest = the base's newest below it, and nothing unexpected above it. Otherwise stop and go to Samson.btree_gistis available, and thatparties,legal_entitiesand thetenant_guardrole exist.anon/authenticated/service_roletable privileges = none;PUBLICcannot EXECUTE the six functions;btree_gistin place. Don't touch other objects, and don't re-run the backlog.§14 Flags
Readybut natively blocked by AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships (P27-S01) #1998, which can't build until [Phase 7] Party spine: parties supertype + persons anchor + households + household_members + org subtype + org_contacts + donors.party_id #570, [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606, [Phase 7] Canonical Legal Entity and immutable issuer-profile runtime foundation (Backlog draft) #1947 and [Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250 land. Samson's call.status:blocked, but that label isn't on the issue. I didn't change labels.support_assignmentsalready exists as the Support Hub table. That's a naming hazard for Phase 21.Closes #1998(if OQ1 is approved) orPart of #1998. Scheduling: Samson's call under the factory routing; labels unchanged.II-ricky-bobby-II commentedon Oct 24, 2026
Spec r1 approved (Samson), pending Ready (prep only), all defaults (OQ1–OQ10). OQ1 (split ruling, under Blake's standing grant of Oct 24, 2026 to split any issue into slices of about 1k lines): #1998 keeps S1, the database layer: 4 tables and 6 functions, writes only through #2250's tenant-guard functions, anon, authenticated and service_role revoked, RLS enabled and forced, no #2253 allowlist growth. That's about 630 lines of code and docs plus about 720 of tests, inside the budget. Scope files S2 (API) and S3 (UI) as not-Ready follow-ups. OQ2: add native blocked-by links to #570, #1947 and #2250. It can't build until #570, #606, #1947 and #2250 are on develop. OQ3: btree_gist with EXCLUDE. Adding the extension is a new dependency, so I'll tell Security Watch when the build adds it. OQ4: only person Parties can be members. OQ5: only a correction can backdate, and it needs a reason. OQ6: a retired assignment refuses new members. OQ7: whichever of #2294 and S1 lands second does the update. OQ8: migration 20261024185000 is provisional and reserved, and Builder re-checks it's unique before handback. OQ9: the auth.users id is the actor until Phase 12 principals exist. OQ10: production ordering follows Blake's containment decision and the backlog-first rule.
II-ricky-bobby-II commentedon Oct 24, 2026
Split recorded (Scope, Oct 24, 2026). Samson approved the split under OQ1 of r1 (#1998 (comment)), using Blake's standing grant of Oct 24, 2026 to split issues into slices of about 1k lines.
20261024185000is provisional and reserved.withTenantand the Phase 12 PDP. Not Ready. It is blocked by AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships (P27-S01) #1998.Labels unchanged; no Ready.
II-ricky-bobby-II commentedon Oct 26, 2026
Pins addendum (Scope, Oct 26, 2026, 8:20 AM PT), pending Samson approval:
20261024185000_ministry_assignments_source.sqlbecomes20261025124500_ministry_assignments_source.sql. The old version sorts below develop's newest (20261025120000, develop88eefbd9).20261025124000and [Phase 7] #570 slice b/2: tenant-guarded party accessors (tenant_guard grants, guarded functions, packages/api/src/party) #227220261025124100([Phase 7] Party spine: parties supertype + persons anchor + households + household_members + org subtype + org_contacts + donors.party_id #570's parties), [Phase 9] #606 slice a/3: parties lifecycle_status, tags, same-kind merge tombstone; spine verification; seed regeneration #227820261025124200and [Phase 9] #606 slice b/3: drop donors.type, organization, spouse; replace donors_assign_party; census v5; minimal excision #227920261025124300([Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606's tombstone), and [Phase 7 #1947a] #1947 follow-up: legal_entities root, idempotent one-default setup and exact Tenant+Entity check #240320261025124400(#1947a).legal_entities (tenant_id, id)FK target now comes from [Phase 7 #1947a] #1947 follow-up: legal_entities root, idempotent one-default setup and exact Tenant+Entity check #2403 (#1947a, spec r1 [Phase 7 #1947a] #1947 follow-up: legal_entities root, idempotent one-default setup and exact Tenant+Entity check #2403 (comment)). The shape is unchanged.20261024212000) and AL-2069 — [P28-W01] Enter and switch the exact authorized workspace context #2069 (20261024215000) must sort after this version and after [#670 (a)] Governance epoch and governance_version: the per-tenant counter #2287's. Each needs its own renumber addendum, which is not posted here.II-ricky-bobby-II commentedon Oct 26, 2026
Pins addendum (Scope, Oct 26, 2026, 8:24 AM PT), approved by Samson under the standing authorization:
parties (tenant_id, id)), [Phase 9] #606 slice a/3: parties lifecycle_status, tags, same-kind merge tombstone; spine verification; seed regeneration #2278 (from [Phase 9] Extend the party spine: lifecycle_status, tags, merge tombstone; regenerate seeds #606:merged_into_party_idtombstone), [Phase 7 #1947a] #1947 follow-up: legal_entities root, idempotent one-default setup and exact Tenant+Entity check #2403 (from [Phase 7] Canonical Legal Entity and immutable issuer-profile runtime foundation (Backlog draft) #1947:legal_entities (tenant_id, id)) and [Phase 4] #505 slice 1/4: withTenant tenant guard (non-owner, non-BYPASSRLS role, fail-closed) #2250 (closed). S1 can't build until [Phase 7] #570 slice a/2: party spine tables, party fill for existing donors, donors.party_id, census v4 and docs #2271, [Phase 9] #606 slice a/3: parties lifecycle_status, tags, same-kind merge tombstone; spine verification; seed regeneration #2278 and [Phase 7 #1947a] #1947 follow-up: legal_entities root, idempotent one-default setup and exact Tenant+Entity check #2403 are on develop.20261025124500(pins addendum AL-1998: [Phase 9] Manage one stable Ministry Assignment and its separate participant memberships (P27-S01) #1998 (comment)). Version is reserved; Samson may renumber at handback to keep merge order.