Design the app framework — retailer and vendor surfaces #23
Notifications
Due Date
No due date set.
Depends on
#10 Define the common schema for RFPs and responses
christian/helmdocs-proposal-system
#11 Define the tenancy, identity, and authorization model
christian/helmdocs-proposal-system
#12 Design the visibility and qualification model
christian/helmdocs-proposal-system
#13 Design the vendor profile and cross-retailer document vault
christian/helmdocs-proposal-system
#22 Collect real retail RFPs and vendor responses
christian/helmdocs-proposal-system
Reference: christian/helmdocs-proposal-system#23
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Question
What are the actual screens, and what is on them? A working design session covering navigation,
the retailer's RFP and evaluation surfaces, the vendor's workspace, and how a single company that
is both a retailer and a vendor moves between the two.
This is a prototype ticket: make something rough and concrete to react to, rather than
deciding in the abstract. Its output feeds the code layout directly — the module structure should
fall out of the surfaces, not be invented alongside them.
Deliberately blocked. The blockers are not bureaucracy; they are the app framework. The
schema determines which objects a screen can show. The tenancy model determines navigation, org
switching, and the dual-role experience. Visibility determines the retailer's core surface, and
the vault determines the vendor's. Designing ahead of these means retrofitting the domain model to
the screens, or discarding the screens.
Also worth having in hand before starting: the artifacts research, for realistic document shapes —
section counts, rubric weightings, pricing-table structures — so the surfaces are drawn against
real documents rather than imagined ones.
Resolution
Prototype: Surface map — six app-framework decisions — every screen below, drawn
against the closed resolutions, with the open tickets marked as seams rather than guessed at.
Six decisions, taken in order, each depending on the one before it. The through-line: the UI
boundary is the authorization boundary, drawn in the same place — and where a surface would have
to invent something the domain model cannot produce, it stops.
1. One app, with a combined org x capability picker
The picker flattens org and capability into a single list —
Acme Foods . Bidding— and the navbeneath it is entirely capability-scoped. Switching context replaces the navigation wholesale.
Why. Tenancy holds that permissions never resolve against an org alone, only against
(org, capability, resource), and that the two contexts cannot see each other. A unified surfacesorted by direction (Incoming / Outgoing) was rejected because
app.capabilitywould then have noUI anchor, and "why can't I see this?" would have no answer the user can point at. Two separate
apps were rejected because they break the one-profile promise experientially and give a retailer
who wants to bid a second product to discover.
What it costs. A dual-role user pays a context switch to move between their own two sides, and
deep links must carry capability or they land in the wrong nav.
Falls out of this, not asked separately: a dual-role org's vendor vault is invisible to its own
issuing context. That is decision 2 of tenancy applied literally, and it is correct — the
issuing side evaluating its own vendor side would be exactly the leak RLS exists to prevent.
2. The vendor's bid surface is requirement-led; uploading is an import path into it
The surface renders the schema directly: one answer per requirement, keyed to
requirement_id,carrying
coverageas answered / not answered / indeterminate. Uploading a PDF fills the cells in;authoring natively fills the same cells. One surface, two entry paths.
Why. Submit from anywhere settled that we accept any document, but never said which
artifact the vendor looks at. A document-led surface (dropzone, background extraction, submit)
was rejected on three counts: native authoring has nowhere to live, so the adoption pull that
decision was counting on never materialises; there is no per-requirement anchor, so the Pro upsell
is homeless; and the vendor never sees what they left unanswered — which the corpus work names as
a first-class case.
What it costs. Extraction failure is loud and in the vendor's face. A bad parse is now the
first thing a paying customer sees.
3. On the retailer's side the document is authoritative; structure is a confirmed overlay
Compose shows the retailer's own RFP beside the requirement overlay extracted from it, provenance
on screen. The overlay governs evaluation but can never silently diverge — editing a row forces
a reissue or becomes a recorded clarification. A retailer with no document authors natively and we
emit the PDF; that is the secondary path.
Why. The schema marks
solicitation_documentlegally authoritative andthe layer decision keeps us out of their system of record. Native-authoring-with-generated-
PDF was rejected because it makes us the system of record for their legal artifact and drags in the
compliance and retention burden that decision explicitly refused — and because a large retailer
will not retype their procurement template. A two-panel both-editable model was rejected because
"legally authoritative" would then have two masters, and a protest asks which artifact governed.
What it costs. Confirmation is real work before every issue, and it sits on the free side.
4. The invitation shows issuer, category and deadline — and nothing else
The full vendor sequence is now invited -> subscribe -> teaser -> acknowledgement -> full RFP:
a billing gate, then a confidentiality gate, doing two different jobs.
Why. Issuer, category and deadline are facts about a relationship the retailer already has with
this vendor offline; scope, volume band and terms are RFP content and stay behind the wall. A bare
"Northgate Market has invited you" was rejected because it reads as phishing, carries no deadline
and therefore no urgency, and leaves the vendor unable to judge whether $1,200/yr is worth it. A
per-RFP toggle was rejected because it puts our conversion rate in the hands of someone with no
stake in it.
What it costs, stated plainly. A forwarded invitation tells a competitor that Northgate is
sourcing Produce. Accepted as the price of legibility.
5. The vendor's home is in-flight work beside eligibility health
Two panels. Deadlines on the left; on the right, what your documents are doing to your invitations
— phrased as consequence, not filing status: not "expires in 11 days" but "you drop out of three
rosters." The anonymised acceptance count lives here too.
Why. The vault established that expiry removes a vendor from future audience
evaluations. That is the cruellest mechanic in the system — a supplier stops being invited and
nothing tells them why. An opportunities-only inbox would ship that failure as a default, and would
also make the thing they pay for a nav item.
What it costs. Two things compete for the eye on load, and the right panel is quiet for a
healthy vendor — dead space most of the time, by design.
6. Evaluation is a requirement matrix, vendors as columns
Requirements down, responses across, coverage state in the cells, with mandatory coverage, per-
scenario cost and exceptions-taken as summary rows.
Why. Because both sides confirmed structure against the same
requirement_id, the comparisonis a join, not a feature — the vendor's surface and the retailer's are the same data seen from
two directions. A ranked leaderboard was rejected as the lead surface on a hard constraint, not a
preference: the schema holds that scores are raw and never normalised, and one composite
number per vendor is precisely that normalisation. It would also hide non-responsiveness behind a
rank. A per-bid dossier was rejected because comparison is the product, and reading one bid at a
time makes a gap in vendor 2 invisible while you are reading vendor 1.
What it costs. A 200-requirement RFP needs column pinning and horizontal scroll; this is the
hardest surface here to make work small.
Seams — deliberately left open
matrix is built to be progressively populated, not populated once.
is that ticket's call; the spine works either way.
retailer's overlay column and the vendor's indeterminate rows.
addresses a roster entry by name, and claiming is what turns it into a vendor org.
already render: document types in the eligibility panel, categories in the picker and invitation,
rate structures in the cost row.
hardest setting, so anything that softens it is a change to that surface, not a new one.
Constraints handed to the code layout
every route carries capability so a deep link cannot land in the wrong context.
overlay, vendor document to answer set. Same shape, same confirm step, one implementation. The
largest shared seam in the system.
entry point: it renders for an actor with no org, no capability and no subscription.
vault decision already required. The vendor home reads whatever that produced.
is derived from code rather than maintained beside it. Visible-but-locked is the default
rendering for a missing entitlement; absent is a bug.
What this makes specifiable
The destination is a code layout, and until now no ticket produced it — four resolutions had
handed structural requirements to an artifact that did not exist. Six surfaces plus five module
boundaries is enough to assemble it. Graduated to its own ticket, blocked on data release since
the permission boundaries are half of what the layout must express.
christian referenced this issue2026-08-03 20:52:58 +00:00