Config
Using Testers is the same ground with the calls around it. Refusals carry an ordinary error code.
testers({
baseUrl: "https://api.example.com",
cohortDefaults: {
targetSize: 12,
windowDays: 14,
maxRosterSize: 100,
},
nudges: { cooldownHours: 72 },
optInLinkTtlDays: 30,
})basePath defaults to /testers.
Every constant behind the forecast is a config field, including the survival priors and the health weights. They are numbers somebody chose, not values fitted to data, and the API says so in a calibration field. Change them and bump modelVersion, so a trend chart annotates where the change took effect.
Roster states
invited | Asked |
accepted | Said yes. Consent, not enrollment |
opted_in | Followed the store link through |
lapsed | |
removed | Taken off the roster |
withdrawn | They opted out themselves |
Activity states
active | Opened the app recently |
inactive | Has not, and we can tell |
never_linked | Never authenticated — we cannot tell. Health is null, and a UI renders this gray, never red |
unreachable | Mail is not getting through |
Nudge kinds
confirm · store · inactive · closing
All four carry an opt-out link.
Routes
| Route | Verified by |
|---|---|
GET /testers/confirm/:token | public — the invitation link |
GET /testers/opt-in/:token | public |
GET, POST /testers/opt-out/:token | public — both verbs |
GET /testers/status | user |
GET /testers/cohorts | control-plane |
POST /testers/invite | control-plane |
POST /testers/resend | control-plane |
POST /testers/remove | control-plane |
POST /testers/nudge | control-plane |
Opting out answers both verbs because a one-click unsubscribe header sends a POST and a person clicking the link sends a GET. Refusing either would refuse somebody trying to leave.
Removal is a POST with a body, not a path parameter — an address in a path is an address in every access log, proxy and trace between the client and the Worker.
Scopes
testers:roster:read · testers:roster:write · testers:nudge:send
Error codes
| Code | |
|---|---|
testers/not_configured | |
testers/cohort_not_found | |
testers/cohort_closed | |
testers/member_not_found | |
testers/already_on_roster | |
testers/withdrawn | |
testers/roster_full | |
testers/invalid_token | |
testers/nudge_cooldown | The per-tester cooldown refused it |
testers/copy_not_allowed |
Which fields are estimates
Why the count is an estimate is the reasoning; what the store APIs withhold is why it has to be.
Every one named estimated. Plus:
| Field | |
|---|---|
successProbability | Nullable — we do not know is representable |
reconciliation.supported | Always false, with the reason attached |
disclaimer | Required on every response |
Days are counted in UTC, and the response says so.
resetPolicy defaults to reset — deliberately pessimistic, because being told day fourteen while actually on day three is the expensive mistake.
What provisioning creates
One Worker, hosting the daily pass, bound as TESTERS_DAILY. No bucket, no index, no secret.
The store’s own limits
| Requirement | 12 testers, 14 continuous days, closed testing only, personal accounts only |
| Closed-test list limits | 2,000 users per list · 50 lists per track · 200 lists per account |
| The “100 testers” figure | Internal testing only. Not this |
The roster cap of 100 is a management judgment, not a store limit.