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.
Each org belongs to one project. Users can be in different orgs across projects.
Org ID, slug, role, and permissions are embedded in the access token. No extra lookups.
Invite by email with a role. Token-based acceptance with configurable expiry.
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.
To enable it, toggle "Organizations" in the project dashboard or use the API:
await fetch("https://astapa.com/api/platform/projects/{id}", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Cookie": "session=...",
},
body: JSON.stringify({ org_enabled: true }),
});Four built-in roles
Every member gets exactly one role per organization. Roles determine which permissions appear in the JWT.
Full control. Can delete the org and transfer ownership. One owner per org.
Invite and remove members, change roles. Can't delete the org.
Read and write access to org resources. The default role for most users.
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:
// 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:
{
"sub": "end-user-uuid",
"org_id": "org-uuid",
"org_slug": "acme-corp",
"org_role": "admin",
"org_permissions": ["read", "write", "invite", "remove_members", "change_roles"],
...
}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:
// 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.
| Permission | Owner | Admin | Member | Viewer |
|---|---|---|---|---|
| read | ✓ | ✓ | ✓ | ✓ |
| write | ✓ | ✓ | ✓ | — |
| invite | ✓ | ✓ | — | — |
| remove_members | ✓ | ✓ | — | — |
| change_roles | ✓ | ✓ | — | — |
| delete_org | ✓ | — | — | — |
| transfer_ownership | ✓ | — | — | — |
org_permissions and decides what to allow.