Story-736 * docs lane * Size XL * Walkthrough

Invitation doctrine cleanup

The Story that removed the fake local invitation model from PRAMAAN's docs.

Pull Request
Built by
Conductor agent on 2026-05-21

The 30-second version

Story-736 fixed a dangerous split in the docs. Some docs said pending invitations lived in PRAMAAN tables. The real target architecture says pending invitations live in Clerk, accepted members live in PRAMAAN Postgres, and audit lives in ops.audit_log. PR #80 added ADR-068, rewrote the canonical invitation doc, deleted the stale hybrid primer, and put warning signs on old docs that future engineers should not implement from.

PART ONE - WHAT WE PLANNED TO DO AND WHY

1 Why does this exist?

Story-736 existed because the docs were teaching two different invitation systems at the same time. One system matched the current direction: Clerk owns pending invitations and PRAMAAN creates local membership rows only after Clerk acceptance. The other system was old: PRAMAAN had an identity.invitations table, invite-time firm_memberships(status='invited') rows, bridge /invitations endpoints, and custom invite-token pages.

That drift was not harmless. Story-705 followed the stale version and built around local invited rows that current code did not write. So Story-736 was a cleanup Story, but the cleanup was really about making the map match the building.

[ ]

Think of a hotel front desk

Clerk is the guest-list system that sends invitations and knows who has accepted. PRAMAAN is the hotel key-card system that gives a room key only after the guest actually checks in. The stale docs said PRAMAAN should print a key card the moment an invitation email was sent. That is how you end up with keys for people who never arrived.

Ask yourself

Why is a stale doc more dangerous than no doc?

Answer: because a stale doc looks authoritative. An engineer or agent may trust it and build a whole feature around a table, endpoint, or state transition that no longer exists. No doc creates uncertainty; a wrong doc creates false confidence.

2 The plan

The plan was to turn a messy doctrine pile into one clean authority chain. ADR-068 would lock the decision. The canonical invitation-and-sign-in doc would explain the current model. Stale historical docs would either get warning banners or be deleted if their whole framing was wrong. The ADR registry would point future readers to the new decision.

The architecture lock had two parts. First: Clerk owns invitation lifecycle. Second: no schema fiction. Schema fiction means a table, column, enum, or identifier that sounds plausible but current code does not actually use.

Inviter staff or firm admin ADR-068 + canonical doc STORY-736 Vercel calls Clerk Postgres after acceptance Clerk pending invite Email acceptance PRAMAAN Postgres STORY-736 contract membership row only after webhook Stale docs banner or delete 1 2 3 4 webhook 5 audit docs 6 no fake invite row

The green boxes are the new Story-736 authority. They are docs, not runtime code, but they control how future runtime work should be understood. The red box is the old material: preserved only with warning banners, or deleted when it taught the wrong system from top to bottom.

What the spec said would ship

The Story asked for ADR-068, a rewrite of invitation-and-signin.md, warning banners on stale docs, deletion of the hybrid invitation primer, an audit pass across many auth-related files, registry updates, and a cross-link from Investigation v2 to ADR-068.

Out of scope

Story-736 did not build app-side Clerk server actions, backend cleanup for Story-705, new invitation webhooks, or a new local invitation table. It fixed the doctrine so those later Stories would not follow the wrong road signs.

PART TWO - HOW WE ACTUALLY DID IT

3 What got built

PR #80 added 366 lines and removed 675 lines across 41 files. The important change was not the raw file count. The important change was the authority chain: ADR first, canonical doc second, stale docs clearly marked as historical, and the misleading primer removed.

Reading order

  1. docs/decisions/adrs/ADR-068-invitation-lifecycle-clerk-owned.md - start with the decision.
  2. docs/system/auth/invitation-and-signin.md - read the canonical operating model.
  3. docs/decisions/adrs/ADR-024-invitation-system.md - see how the old ADR was redirected without rewriting history.
  4. docs/decisions/adrs/ADR-REGISTRY.md and adr-registry.json - confirm discoverability.
  5. docs/investigations/2026-05-20-invitation-architecture-v2.md - understand the evidence trail.
  6. Bannered docs such as member-lifecycle.md, firm-creation.md, and invitation-flow diagrams - see what future readers must not implement from blindly.
  7. The deleted docs/primers/technical/authentication/invitation-architecture.md in PR diff - understand why the hybrid model was removed instead of patched.
ADR-068-invitation-lifecycle-clerk-owned.mdDecision lock

This is the main artifact. It says pending invitations belong to Clerk. PRAMAAN Postgres has no identity.invitations table and no identity.firm_memberships row before Clerk acceptance. It also introduces the no-schema-fiction rule: if current code does not use a schema element, the docs and schema should not pretend it is part of the product contract.

Ask yourself

Why put the invitation lifecycle and no-schema-fiction rule in one ADR?

Because: the invitation bug was caused by schema fiction. The stale invitation model looked real because old docs described local rows and local invitation tables. The lifecycle decision and the schema-discipline rule fix the same failure mode from two sides.

invitation-and-signin.mdCanonical guide

This doc became the plain-English operating guide. It states the three-row mental model: pending invitation equals Clerk object; accepted member equals PRAMAAN membership row; invitation audit equals ops.audit_log event.

It also names the implementation rules future code must obey: Vercel server actions call Clerk for invite, resend, revoke, and pending-list actions; the bridge has no /invitations endpoints; Lambda must not call Clerk; accepted-member actions use PRAMAAN identifiers.

ADR-024-invitation-system.mdHistorical redirect

ADR-024 was not edited into a new decision. Accepted ADRs are history. Story-736 updated the supersession banner so readers know ADR-024's first-class invitation-table model is no longer execution authority and should be read through ADR-068.

This is like putting a bright notice on an old building plan: keep the plan for evidence, but do not hand it to the electrician as today's wiring diagram.

ADR registriesDiscoverability

ADR-REGISTRY.md and adr-registry.json were updated so humans and tooling can find ADR-068. This matters because a decision that exists but cannot be found is only slightly better than a decision that was never written.

warning bannersSafety tape

Files that still described pre-ADR-068 invitation behavior got warning banners. That includes docs about old invite-token pages, bridge-side invitation examples, old firm-onboarding flows, and roadmap or sprint docs that mentioned local invitation state.

The goal was not to erase every historical sentence. The goal was to make the first thing a reader sees say: this is stale, here is the current authority, do not implement from this file without checking ADR-068.

Gotcha

A bannered file can still contain old words like /invite/[token] or /invitations. That is intentional historical preservation. The banner is the authority gate.

invitation-architecture.mdDeleted primer

The hybrid invitation primer was deleted because its core framing was the bug. It did not need a banner plus small edits. It taught readers to blend Clerk and PRAMAAN local invitation state, which is exactly what ADR-068 rejects.

4 Deviations from the plan

The core plan held: ADR-068 landed, the canonical doc was rewritten, stale docs were marked, the hybrid primer was deleted, registries were updated, and investigations were cross-linked.

The main practical expansion was the audit surface. PR #80 bannered more files than the short "7 warning files plus delete" list because the audit found more stale invitation language in diagrams, bridge utility primers, roadmap notes, sprint notes, and system docs. That was a good expansion: once you find the wrong road sign repeated in multiple corridors, you do not leave half of them standing.

One planned idea stayed deferred: removing any dead invited membership status allowance from schema. Story-736 was docs doctrine. A follow-up runtime/schema cleanup belongs in the repo that owns the executable schema change.

5 Errors hit and how we fixed them

The visible PR record does not show a dramatic build failure. This was a documentation architecture Story, so the real error being fixed was upstream: Story-705 had trusted stale doctrine and built toward local invitation state. Story-736 fixed that by creating a higher-authority ADR and redirecting the old material.

The second error class was easy to miss: stale docs were not all in one folder. Invitation claims appeared in auth doctrine, frontend utility primers, bridge utility primers, diagrams, workflows, roadmaps, sprints, and system architecture notes. The fix was an audit pass, not a narrow edit.

Ask yourself

Why not just rewrite every stale file instead of adding warning banners?

Answer: because some files are historical evidence or old plans. Rewriting all of them would blur the record and risk changing stable paths. A warning banner keeps the evidence but tells future builders where the current truth lives.

6 Gotchas and surprises

Gotcha

firm-creation.md can still look official because it has old canonical language below the banner. For invitation behavior, ADR-068 wins.

Gotcha

Do not search for old words and assume every hit is a bug. Some hits are negative statements, ADR history, or investigation evidence. Read the surrounding paragraph before changing anything.

Gotcha

Pending invitation IDs are Clerk orginv_* IDs. Accepted member actions use PRAMAAN user_uuid or userId. Mixing those IDs is like using a shipping tracking number as a hotel room key.

Gotcha

The bridge still owns accepted-member operations, but it must not grow new Clerk invitation endpoints. Clerk calls stay in Vercel server actions under ADR-022.

7 What's still open?

First, runtime work remains separate. App-side Clerk server actions for invite, resend, revoke, and pending-list behavior still need their own implementation Stories if not already shipped.

Second, schema cleanup may still be needed if any active schema allows an invited membership status that no code writes. ADR-068 says stale schema must be removed in the same PR or an immediately following cleanup PR.

Third, direct navigation to app.pramaan.io has an open product/runtime check: the active Clerk configuration must confirm whether sign-in auto-matches pending organization invitations by email or whether PRAMAAN needs a post-sign-in pending-invitations screen.

Finally, future PRs must remember that warnings are not a permanent substitute for current docs. If a bannered historical doc becomes important again, either rewrite it under ADR-068 or create a new current page.

8 Check yourself

Can you answer these?

  1. Where does a pending firm invitation live after Story-736?
  2. When does PRAMAAN create an identity.firm_memberships row?
  3. Why is an identity.invitations table rejected for v1?
  4. Which surface calls the Clerk SDK for invite, resend, revoke, and pending-list operations?
  5. Why did Story-736 delete the hybrid invitation primer instead of just adding one warning paragraph?
  6. What should you do when a bannered file still contains old /invitations language?
  7. How does the no-schema-fiction rule prevent the Story-705 failure from repeating?

If those answers are clear, you can safely read or extend this area. If they are fuzzy, start again from ADR-068 and the rewritten invitation-and-sign-in doc before touching code.