Module

Organization

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

terminal
pithy add organization

What it is for

Any 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

Your D1, beside the auth tables.

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.

  • D1
  • Workers

The honest bit

It proves the membership. It cannot scope your tables for you.

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

A members table is an afternoon. The refusals are the rest of the month.

Every multi-tenant product writes this model. The part that takes the time is what a membership is worth, and what it refuses.

A refusal that is not an oracle

An account you are not in has to answer exactly like one that does not exist, or iterating ids reads out the customer list.

Roles you can change your mind about

A catalog declared once, decoded on every read, with nesting checked where it is claimed and a last-administrator floor that holds under concurrency.

Invitations a forwarded link cannot redeem

Single use as a property of the write, one live link per address, and an accept that needs the invited address's own session.

Ownership nobody is handed by a click

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.

A suspension that stops at the account

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.

An audit trail with no address in it

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.

terminal
pithy add organization

In the dashboard

Administer it from a browser, without building the screen.

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.

Add it, and get on with the feature.

One command installs the package, wires its bindings into every environment your project declares, and runs its migrations.