Schema
A personal account is a normal single-member org flagged via
metadata. It is capped at a single profile (profiles.create rejects a second profile with PROFILE_LIMIT_EXCEEDED; the cap is an ≤ 1 existence check). Team orgs are uncapped. Creating any org auto-seeds one default server_managed profile (name and externalId both "default") in the same transaction, so a fresh org is immediately usable. A personal account is not in custody mode — its owner can reveal their own item values (items.ownerReveal for server-managed, in-browser decrypt for ZK); team orgs stay in custody mode and the dashboard never reveals plaintext there.
Membership
Users join organizations through themember table. Roles are:
Member emails are visible to
owner and admin roles only. member-role callers see null for email — including their own row. Users should read their own email from their session.Invitations
Invitations are one-time tokens (abi_...) shown once and stored as SHA-256 hashes. They expire after 7 days.
The flow:
1
Admin creates invite
organizations.members.invite returns a token. The frontend builds the URL: {APP_URL}/invite/accept?token={token}.2
Recipient looks up the invite
organizations.members.getInviteInfo reveals the org name, role, and expiry. Authentication is required to prevent enumeration.3
Recipient accepts
organizations.members.acceptInvite adds them to the organization with the invite’s role. The token is consumed atomically.Cascades
Deleting an organization cascades to its agents, permissions, profiles, and items. The audit log retains entries (no FK constraints). Removing a member cascades agent revocation for agents owned by that member.In the dashboard
Switch the active organization using the org switcher in the dashboard header. Operations on profiles, items, agents, and permissions act on the active organization. In the CLI, set the active organization with:apiUrl and activeOrgId are persisted in ~/.abadge/config.json.