Tenancy in your own D1. Organizations, the people in them, the roles you declare and the powers those roles hold, invitations bound to an address, ownership that moves only when somebody accepts it, and a record of which account each session is acting in.
@pithy-sh/organization
pithy add organizationAny product where more than one person shares an account and they should not all be able to do the same things: a team workspace, a school with teachers and students, an agency running its clients.
You declare the roles and the powers they hold. A dashboard's member, admin and owner nest; a coaching academy's coach and student do not. Both are legal, and your own routes gate on the same powers the kit's routes do.
Before an operator suspends or deletes an account, they can preview who it would reach, member by member. The preview runs the same selection the write does, and writes nothing.
An agent a member connects through mcp() gets exactly that member's powers in the organization they are acting in. Its tools are filtered by the same role check each route makes, so demoting the member takes the tools away on the next call, with no deploy.
Hear about a demotion or a departure. onMemberRoleChanged and onMemberRemoved fire from every route that changes a role or ends a membership, and a role change carries the powers it lost, so a management client can take back what that person authorized.
What it sits on
Six tables, all pithy_organization_*, in the same database as auth's, because a membership is keyed by a user id auth owns and the roster resolves people through auth's published reader.
Nothing about a role rides on the session — only the id of the organization in force — so deleting a membership row is the whole of revocation.
The honest bit
The gates put a proved membership on the request and refuse everybody else with a 404 that cannot be told from a missing account. Past that, your own tenanted tables are yours: every query against one still has to join memberships on the user and the organization, and this capability has no view of them to do it for you.
It is not Better Auth's organization() plugin, and composing both in one Worker is refused. It has no teams, no organizations inside organizations, no SSO or SCIM, and no billing of its own — payments bills an organization; this says which one.
If you build it yourself
Every multi-tenant product writes this model. The part that takes the time is what a membership is worth, and what it refuses.
An account you are not in has to answer exactly like one that does not exist, or iterating ids reads out the customer list.
A catalog declared once, decoded on every read, with nesting checked where it is claimed and a last-administrator floor that holds under concurrency.
Single use as a property of the write, one live link per address, and an accept that needs the invited address's own session.
An offer and an acceptance, in one write that never leaves two owners or none — and a break-glass path for support when the owner has gone.
Shut one tenant without locking its people out of the others they work in, reach a member's own account only when this was their one organization, and erase a deleted tenant's rows and stored bytes from every capability that held them.
Every membership change recorded with its actor, and the operator's acts with their reason, without the trail becoming a directory of who you mailed.
pithy add organizationComposes with
Passwordless sessions, mobile and web both first-class. Magic link, email OTP, Google, Apple, GitHub, Facebook, Microsoft.
Every send is a durable job: scheduled, retried, tracked. No mail ever sent inline.
A queryable trail of every security-relevant action, attributed to the right actor.
Let a management client reach into your own Worker, with no data plane in between.
In the dashboard
Every organization your Worker holds, its roster and its standing, readable on every plan. On Team, suspend, reinstate, delete or restore one, with a preview of whose own accounts the cascade would reach, and add, remove or re-role a member, fix an invitation, set a seat limit or force ownership, each with a reason.
The dashboard is the admin interface for your own application: whatever this capability writes, your team can look up, check and act on from a browser. Reading is free, forever, with no time limit. Connect the project when you deploy.
Everything else
Each one composes the same way. Nothing you skip costs you anything.
One command installs the package, wires its bindings into every environment your project declares, and runs its migrations.