# Retail RFP transmission system — v1 layout A distribution and evaluation layer for retail sourcing. Large retailers issue RFPs and evaluate bids for free; vendors pay for the workspace that helps them win. This repository is the **v1 code layout**: directory and module structure, the domain model as real schema files, permission boundaries expressed as code, and typed seams where the undecided parts will land. **It is not a running product.** It is the skeleton a build can start from without being redesigned. Decisions live on the issue tracker, indexed by [the map](https://gitea.stephenmann.io/christian/helmdocs-proposal-system/issues/1). Nearly every file here carries a comment naming the ticket that fixed it — if a choice looks arbitrary, the ticket says why, and what it cost. ## The four structural requirements this layout exists to satisfy Four resolutions handed requirements to a layout that did not exist yet. They are the reason this repository is shaped the way it is. | Requirement | Where it lives | From | |---|---|---| | A single database entry point that makes bypassing actor context awkward | `src/db/index.ts` — the pool is not exported | #11 | | A scheduled-job entry point establishing actor context with no request | `src/scheduler/` | #13 | | An entitlement assertion helper carrying the convenience-not-correctness principle | `src/auth/entitlement.ts` | #20, #34 | | An explicit, reviewable list of privileged RLS bypasses | `src/db/privileged.ts` — three of them | #33 | ## Layout ``` src/ db/ index.ts THE database entry point. withActor() is the only way in. actor.ts ActorContext; SET LOCAL discipline privileged.ts the enumerated RLS bypasses — keep this short schema/ the domain model, with RLS policies beside their tables auth/ entitlement.ts assert per feature; its call sites ARE the paid-feature list release/ matrix.ts the release matrix, mirrored from SQL so screen and policy agree scoring/ routing.ts what a model may decide. Two lanes of six. rank.ts ranking over committed scores only extraction/ index.ts ONE module used twice — RFP and response completeness.ts the second, INDEPENDENT pass. Must share nothing with the above. scheduler/ jobs, with actor context and no request http/ context.ts request -> actor context; entitlement resolved once routes/ two capability-scoped nav trees, plus a public pre-auth surface pricing/ rate function evaluation — deterministic, no model fallback audit/ integration/ helmdocs/ kickoff/ claiming/ seams migrations/ 0000_* SQL functions, hand-authored — must precede the policies 0001_* generated schema, with RLS policies beside their tables 0002_* the release matrix seed, hand-authored docs/ seams.md what is open, and what is deliberately closed migrations.md why policies sit beside tables and the matrix is a data migration ``` ## Three invariants worth knowing before you touch anything **Permissions never resolve against an org alone.** Always `(org, capability, resource)`. An org may hold both the issuer and vendor capability, and those are two authorization contexts that cannot see each other — a dual-role company's vendor vault is invisible to its own issuing side. This is enforced in Postgres, not in application code, because the invariant that a vendor's bid is never reachable by another vendor is the one failure that would destroy the product's credibility. **Entitlement is not row visibility.** RLS answers *whose row is this*; a lapsed vendor still owns every row they owned yesterday. Policies must not reference entitlement at all. Entitlement gates what a vendor may newly undertake, never what a retailer has already come to rely on. **Bids are sealed until the deadline, then the whole field opens at once.** Bid shopping is structurally impossible rather than discouraged. A surprising amount depends on this: it is also what makes mid-flight amendments safe, since the retailer has read nothing and cannot tailor one to advantage a response they have already seen. ## Working on a seam ``` yarn seams ``` Every seam throws with the ticket that owns it. Replace the body; the signature is already the handoff. If you find yourself changing a seam's *types*, check the ticket first — the shape usually came out of a resolution rather than out of convenience.