Canonical schedule
Published round dates live in one file — the single place a human edits.
Most portfolios show the happy path on a nice background. This is the other eighty percent — the end-to-end flows, the permission models, and what a product does at 2am when everything goes wrong.
EQUIP is BioHubNet's funding programme: micro-grants for Canadian biotech founders. Designing it meant designing a relay — a founder who has never logged in hands off to a reviewer, who hands off to a committee, who hands off to a public record. Every handoff is a place the flow can break, so every handoff got a designed failure state, not just a happy one.
A public page shows the live deadline in Toronto time, derived from the canonical round schedule — never hand-typed.
One form, two streams — VentureConnect (monthly) and VentureLift (rounds), each with its own cap and cadence.
Reviewers see a queue scoped to their committee — not an admin console with everything in it.
Decisions are recorded with who and when, so the audit trail is a by-product of the flow, not extra admin.
Funded companies roll into a recipient dossier — one curated dataset behind every view.
An unlisted link opens the report with no login, for a partner who will never have an account.
The last two columns are the ones that matter. A flow that only works for people with accounts isn't finished — most of the humans in this programme never log in once.
The platform isn't an app, it's a suite: learning, placement, funding, hiring, and the admin surface behind all of it. The design problem is that a trainee, an employer, and a grant reviewer all live in the same product and must never feel like they wandered into someone else's tool.
A funding deadline appears in four places: the public window, the applicant's submit gate, the admin calendar, and the reviewer's queue. Typing it four times guarantees they drift. So the schedule is declared once and everything else is derived from it.
Published round dates live in one file — the single place a human edits.
Each round expands into the windows it implies, in Toronto time.
Stale auto-rows are removed, missing ones inserted, edited ones left alone.
The same computed instant closes the form and greys the button.
This started as a bug: a “last Monday of the month” rule silently disagreed with the published dates for three months of the year. The fix wasn't a better rule — it was deleting the rule and making the published dates the source.
Most products pick one permission model and bend everything to fit. This one genuinely needed three, because the humans are different: staff who belong to an org chart, reviewers who belong to a committee, and partners who belong to nothing at all and still need to open a document.
A ranked role chain — trainee, employer, instructor, admin, superadmin. Higher ranks inherit everything below. Right for staff surfaces where seniority genuinely nests.
requireRole("admin")
Rank breaks down for a grant committee: a reviewer isn't “more senior,” they're assigned. So access is membership-or-role — you're in the committee, or you're an admin.
requireCommitteeOrAdmin([…], […])
For people with no account, the URL is the credential. A 128-bit token grants exactly one document, carries its own edit flag and expiry, and dies when revoked.
/share/<token>
| Capability | Ladder (role) | Membership (committee) | Token (link) |
|---|---|---|---|
| Read a grant queue | Admin+ | Assigned reviewer | — |
| Decide an application | Admin+ | Assigned reviewer | — |
| Read the recipient report | Admin+ | — | Anyone with the link |
| Mint / revoke a share link | Admin only | — | Never self-issuing |
| Edit a shared contact list | Admin | — | Only if the link says so |
| Write through the agent API | — | — | Token + explicit env flag |
Every one of these fails closed. No token configured means writes are refused, not allowed. A missing committee redirects rather than rendering an empty shell. And a capability link can only ever be issued by someone higher up the ladder — a token can never mint another token. Permission design is mostly deciding what happens when the answer is “I don't know.”
The states below are the ones users only meet when something has already gone wrong — a dead link, a failed save, a half-filled form about to go to a real partner. They're the least glamorous screens in the product and the ones that decide whether it feels trustworthy.
One confirmation pattern for everything is either exhausting or dangerous. Friction is matched to how hard the action is to undo.
Optimistic UI is a lie unless it can take itself back. If the request fails, the interface reverts and says so — instead of showing a confident “Saved”.
A campaign template with unfilled fields produces “closes .” in a real partner's inbox. The gap is caught while it's still fixable.
A dead control that won't say why is a bug report waiting to happen. The blocker and the fix travel with the button.
A link that timed out is a person who needs a fresh one. A link that was revoked shouldn't confirm it ever existed. Same dead end, deliberately different doors.
Every list gets a composed zero-state that says what belongs here and how to put the first thing in it.
Every value a human types twice will eventually disagree with itself. Declare it once and compute the rest — most “data bugs” are really design decisions made too late.
When the system doesn't know whether you're allowed, the safe answer is no — but it owes you a reason. Silence is what makes software feel hostile.
The most important reader of a document is often someone who will never sign up. If the flow ends at a login wall, the flow isn't done.