Docs/Organizations

Organizations

Let your end users work as teams. Each organization has members with roles, and the org context flows into JWT tokens automatically — so your app can enforce permissions without extra API calls.

How it works

Organizations are scoped to a project. End users can belong to multiple orgs, each with a different role. When a user authenticates with an org_id, the JWT includes org context your app can use for access control.

Project-scoped

Each org belongs to one project. Users can be in different orgs across projects.

JWT-native

Org ID, slug, role, and permissions are embedded in the access token. No extra lookups.

Invitation flow

Invite by email with a role. Token-based acceptance with configurable expiry.

Backward compatible

Omit org_id from the token request and everything works as before.

Hosted login — org picker

When org_enabled is turned on for a project, the hosted login flow adds an organization picker step after authentication. Users who belong to multiple orgs choose which one to sign in with, and the selected org context is included in the authorization code.

Login flow with org_enabled
LoginPlan select (if enabled)Org selectRedirect with code

To enable it, toggle "Organizations" in the project dashboard or use the API:

enable-org-picker.tstypescript
await fetch("https://astapa.com/api/platform/projects/{id}", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    "Cookie": "session=...",
  },
  body: JSON.stringify({ org_enabled: true }),
});
Users without orgs skip the picker
If a user doesn't belong to any organization, the org picker step is automatically skipped and they proceed straight to the redirect. Users can also click "Skip" to continue without selecting an org.

Four built-in roles

Every member gets exactly one role per organization. Roles determine which permissions appear in the JWT.

Owner

Full control. Can delete the org and transfer ownership. One owner per org.

Admin

Invite and remove members, change roles. Can't delete the org.

Member

Read and write access to org resources. The default role for most users.

Viewer

Read-only access. Perfect for stakeholders and external collaborators.

Org context in JWT tokens

Pass org_id in the token request to include org claims in the access token:

token-request.tstypescript
// Exchange code for token with org context
const res = await fetch("https://astapa.com/api/platform/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code: "auth_code_here",
    client_id: "proj_...",
    client_secret: "secret_here",
    redirect_uri: "https://yourapp.com/callback",
    org_id: "org-uuid-here"  // ← add this
  })
});

The resulting JWT will include these additional claims:

jwt-payload.jsonjson
{
  "sub": "end-user-uuid",
  "org_id": "org-uuid",
  "org_slug": "acme-corp",
  "org_role": "admin",
  "org_permissions": ["read", "write", "invite", "remove_members", "change_roles"],
  ...
}
Use org_permissions, not org_role
Check org_permissions in your app logic instead of the role string. This way, if we add new roles later, your code still works.

Quick start

Three API calls to go from zero to org-scoped tokens:

quick-start.tstypescript
// 1. Create an organization
const org = await fetch(
  "https://astapa.com/api/platform/projects/{projectId}/orgs",
  {
    method: "POST",
    headers: { Cookie: "session=...", "Content-Type": "application/json" },
    body: JSON.stringify({
      name: "Acme Corp",
      slug: "acme-corp",
      creator_end_user_id: "end-user-uuid"
    })
  }
);

// 2. Invite a team member
const invite = await fetch(
  "https://astapa.com/api/platform/projects/{projectId}/orgs/{orgId}/invitations",
  {
    method: "POST",
    headers: { Cookie: "session=...", "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "teammate@example.com",
      role: "member",
      invited_by: "end-user-uuid"
    })
  }
);

// 3. Request a token scoped to the org
const token = await fetch("https://astapa.com/api/platform/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "refresh_token",
    refresh_token: "...",
    client_id: "proj_...",
    client_secret: "...",
    org_id: "org-uuid"
  })
});

Role permissions matrix

These permissions are included in the JWT org_permissions array. Your app decides what each permission means for your resources.

PermissionOwnerAdminMemberViewer
read
write
invite
remove_members
change_roles
delete_org
transfer_ownership
Permissions are advisory
Astapa includes permissions in the JWT but doesn't enforce them on your resources. Your app reads org_permissions and decides what to allow.

API reference — Organizations

API reference — Members

API reference — Invitations

Next steps

API Playground
Click "Try it" on any endpoint to get started.
Organizations | Astapa