On this page

Organizations and teams

Teams and workspaces for your app's users: organizations, members, roles and e-mail invites, with JWT claims and ready helpers for Row Level Security.

Most B2B apps need the same thing: a user creates a team, invites colleagues by e-mail, gives them roles, and every row is visible only to its team. Turn on Organizations and StitchDB adds all of it to your database: tables, invite e-mails, functions your app calls with the SDK, a role claim in the JWT and helpers for your policies.

#Turn it on

In Studio open Authentication → Organizations and choose Turn on organizations. This installs:

WhatWhere
Organizations, members, invites, settingsTables in the orgs schema. Not served by the REST API; members can read their own organizations and fellow members.
Functions for your apppublic.org_create, org_invite, org_accept_invite, org_set_role and the others below, for signed-in users only
Policy helpersauth.org_id(), auth.is_member(org), auth.has_role(role, org), auth.org_role(org)
JWT claimsorg_id, org_role and orgs (every organization id with the user's role), through the access token hook
Invite e-mailsSent with your project's Auth e-mail settings: your own domain or SMTP server if you set one up

Turning it off later keeps the schema and data (your app keeps working) and only stops invite e-mails and the JWT claims. You can also remove everything.

#Roles

Every organization has at least one owner. The built-in roles rank owner > admin > member: auth.has_role('admin', org) is true for admins and owners. Add your own roles (for example billing, viewer) in the Settings tab; they count as members and auth.has_role('billing', org) matches them exactly.

WhoCan
ownerEverything, including deleting the organization and transferring ownership
adminRename, invite (up to admin), change and remove members below admin, revoke invites
member and custom rolesSee the organization and its members, leave

#Use it from your app

import { createClient } from 'stitchdb-js'
const stitchdb = createClient(url, anonKey)

// The signed-in user creates an organization and becomes its owner
const org = await stitchdb.orgs.create({ name: 'Acme', slug: 'acme' })

// Invite a colleague: they get an e-mail with a link to your app
await stitchdb.orgs.invite(org.id, { email: '[email protected]', role: 'admin' })

// On the page the invite link opens, once Bob is signed in with that address
const token = new URLSearchParams(location.search).get('org_invite')
await stitchdb.orgs.acceptInvite(token)

// Organizations of the user, members, roles
const orgs = await stitchdb.orgs.list()
const members = await stitchdb.orgs.members(org.id)
await stitchdb.orgs.setRole(org.id, userId, 'member')

// Switch the active organization: the next JWT carries org_id and org_role
await stitchdb.orgs.setActive(org.id)
FunctionWhoReturns
org_create(name, slug?, metadata?)Any signed-in user (or only your server, see Settings)The organization
org_update(org, name?, slug?, metadata?)adminThe organization
org_delete(org)owner—
org_list()MemberThe user's organizations with role and active
org_members(org)MemberUser id, e-mail, name, role, joined
org_invite(org, email, role?)adminThe invite, with its token
org_invites(org), org_revoke_invite(invite)adminOpen invites / —
org_accept_invite(token)The invited address, signed inThe organization
org_set_role(org, user_id, role), org_remove_member(org, user_id)admin (owner for admins)—
org_leave(org), org_set_active(org)Member—
org_transfer_ownership(org, user_id)owner—

#Invite e-mails

An invite e-mail links to your Site URL with ?org_invite=<token>, or to your own page if you set Invite link (for example https://app.example.com/join/{token}). The invited person signs in or signs up with the invited address and your page calls acceptInvite(token). Invites expire after 7 days by default. org_invite also returns the token, so you can share the link yourself instead.

#Row Level Security

SQL
create table projects (id bigint generated always as identity primary key, org_id uuid not null, name text);
alter table projects enable row level security;

-- Members read their organizations' rows
create policy "members read" on projects for select to authenticated using (auth.is_member(org_id));

-- Admins and owners write them
create policy "admins write" on projects for all to authenticated
  using (auth.has_role('admin', org_id)) with check (auth.has_role('admin', org_id));

-- Or only the active organization
create policy "active org" on projects for select to authenticated using (org_id = auth.org_id());

The helpers look up membership live, so removing a member or changing a role takes effect right away, even while the user still has an older token. auth.org_id() is the organization of the request: the x-org-id header if the user is a member of it, else the org_id claim of the JWT.

#JWT claims

JSON
{
  "sub": "8f0c…",
  "role": "authenticated",
  "org_id": "5b1e…",
  "org_role": "admin",
  "orgs": { "5b1e…": "admin", "c27a…": "member" }
}

The claims are added by the access token hook pg-functions://postgres/orgs/access_token_hook, set when you turn organizations on. If your project already has its own access token hook, it stays and the org claims are not added; call the helper from your hook instead:

SQL
create or replace function public.my_access_token_hook(event jsonb) returns jsonb language plpgsql stable as $$
begin
  event := orgs.add_claims(event);
  -- your own claims here
  return event;
end $$;

See Auth hooks. Use the claims for UI (which organization is active); use the helpers in policies.