pithy add testers --with-prerequisites--with-prerequisites composes email, which testers requires — an invitation is a message, and the whole roster is worked by mail. That in turn brings secrets, since email requires it.
Add auth too. It is what turns who did we invite into who has actually opened the app, which is the half of this capability nobody else has.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration, and baseUrl is the one field you must set before anything can be sent:
testers({
basePath: "/testers",
baseUrl: "https://example.com",
modelVersion: "1",
}),apps/<worker>/wrangler.jsonc gains the DB binding, in every environment stanza. The daily-pass Workflow binding is optional and arrives with provision.
One migration runs, creating the cohort, roster, event and snapshot tables.
baseUrl gates every send
The opt-in link is built from it, and an invitation cannot go out without one — an email carrying a relative link is an email nobody can act on.
basePath matters for the same reason, with a sharper edge: change it and the confirmation links in mail already sent stop working. Those URLs are in inboxes and cannot be recalled. Pick it once.
The three cohort defaults
pithy testers create freezes its target, window, roster cap, platform and reset policy onto the cohort row — read from the capability’s defaults unless a flag overrides them.
| Default | Value | Why |
|---|---|---|
| Target size | 12 | Play’s floor |
| Window | 14 days | Play’s window |
| Roster cap | 100 | A sensible ceiling |
| Platform | android | The only store with this requirement |
They are stored rather than read from config at query time, and that is deliberate: raising the target from twelve to fifteen later would otherwise retroactively rewrite whether last Tuesday counted.
Then provision
pithy testers provision --env prodThat deploys the daily-pass Workflow worker and writes its binding into every environment stanza. The pass runs daily at 05:00 UTC.
It resolves two ids first: the app DB id from that environment’s stanza, and this project’s shared email-suppression database — the pass reads it to reconcile bounced addresses, so pithy email provision has to have run.
The sending identity is read off the composed email capability rather than asked for. With no email capability composed, the host still deploys and the pass records the day but sends nothing — said at provision time rather than discovered on the first silent morning.
Running a cohort
The whole loop is five commands.
pithy testers create closed-test --store-url https://play.google.com/apps/testing/com.example.app
pithy testers invite closed-test --email ada@example.com --email grace@example.com
pithy testers run --cohort closed-test
pithy testers pending closed-test
pithy testers status closed-testinvite adds people and sends nothing. run is what asks them whether they will test.
pending prints the addresses that have agreed and are waiting for you to add them to Play’s own tester list — the one step that cannot be automated, because the Play Developer API has no way to add an address to an email list. Until you do it, the store link answers App not available.
What the numbers mean
Every opt-in figure is an estimate from your own invite records. Google’s count is authoritative and no API exposes it, so every derived figure is named estimated and the response carries that disclaimer as a required field.
The activity half is different: it is read from the auth tables and carries source: "observed". It is a fact about your app rather than a guess about Google, and it is what shows you a tester who has gone quiet on day eight instead of on day fourteen.
The two blocks never blend, and each carries its own discriminator so nothing downstream can mistake one for the other.
Check it worked
pithy doctor reports testers under the Worker’s health.
The roster commands run entirely locally at the default --env dev, against the same Miniflare state pithy dev uses — so create a cohort, invite two seeded users and run the pass without touching an account at all.