Skip to content

Integrate Your App

This guide shows the shortest practical path from an existing business API to a working Plystra authorization check.

By the end, your backend can ask Plystra:

Can Alice, acting as Finance Reviewer, approve expense_report_001 in Finance APAC?

and get an explainable allow or deny decision with an audit trace.

For a first integration, keep your existing app as the source of truth:

your users table -> inline actor
your organizations or tenants table -> inline space_id
your memberships table -> inline member_id and binding_id
your invoice table -> inline resource
your role table -> inline grants

Context Mode does not require importing users, organizations, memberships, or resources into Plystra. Your backend sends the relevant decision context at the moment it protects a business action.

This is a trust boundary. Inline actor, resource, and grant fields are accepted only from server-to-server API key calls. A browser or mobile client must call your backend first; your backend constructs the Plystra request from trusted session/database state.

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/authz/check" \
-H "Content-Type: application/json" \
-H "X-Plystra-API-Key: $PLYSTRA_API_KEY" \
-d '{
"actor": {
"user_id": "user_external_alice",
"member_id": "member_finance_reviewer",
"binding_id": "binding_external_alice_finance",
"space_id": "space_acme",
"user_email": "[email protected]"
},
"resource": {
"type": "invoice",
"external_id": "invoice_001",
"space_id": "space_acme",
"group_path": "finance.apac",
"owner_member_id": "member_invoice_creator"
},
"grants": [{
"role_key": "finance_approver",
"resource": "invoice",
"action": "approve",
"scope": "group_tree",
"space_id": "space_acme",
"scope_anchor_group_path": "finance"
}],
"action": "approve"
}'

The same shape works through all official SDKs. Use /api/v1/authz/explain to return the full trace.

{
"data": {
"decision": "allow",
"deny_code": null,
"reason": "at least one matching permission grant covers the target resource",
"trace_id": "trc_..."
}
}

Common first denies:

CodeMeaning
INLINE_CONTEXT_REQUIRES_API_KEYInline context was sent with a session or browser credential.
ADMIN_PERMISSION_REQUIREDThe API key does not have authz:check for the inline Space or Group.
NO_MATCHING_PERMISSIONNo inline grant matched the requested resource/action.
SCOPE_OUT_OF_BOUNDSA matching grant exists but the resource is outside its scope anchor.
CROSS_SPACE_VIOLATIONActor, resource, grant, or scope anchor Space IDs do not agree.

After Context Mode is working, the managed Core model below is available when you want Plystra to store Spaces, Groups, Members, Resource records, and Role grants directly.

Map your application concepts to Plystra first:

Your applicationPlystra CoreExample
Tenant, company, workspaceSpacespace_contoso
Department, project, folder, org unitGroupfinance.apac
Login accountUseruser_docs_alice
Business actor inside a tenantMembermember_docs_finance_reviewer
User can act as MemberUserMemberum_docs_alice_finance_reviewer
Business objectResourceexpense_report_001
Business object typeResourceTypeexpense_report
Business operationResourceActionapprove
Permission rulePermissionexpense_report.approve.group_tree
Role assignmentMemberRoleFinance Reviewer has Finance Approver in finance

The important identity rule is:

User -> UserMember -> Member -> Space

Your backend should send that actor tuple to Plystra when it protects a business operation.

For local development, login as the seeded Alice super admin and export the returned access token:

Terminal window
export PLYSTRA_URL=http://localhost:8080
export PLYSTRA_ACCESS_TOKEN=$(
curl -s -X POST "$PLYSTRA_URL/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"plystra-demo"}' |
jq -r '.data.access_token'
)

PowerShell:

Terminal window
$env:PLYSTRA_URL = "http://localhost:8080"
$login = curl.exe -s -X POST "$env:PLYSTRA_URL/api/v1/auth/login" `
-H "Content-Type: application/json" `
-d '{"email":"[email protected]","password":"plystra-demo"}' | ConvertFrom-Json
$env:PLYSTRA_ACCESS_TOKEN = $login.data.access_token

All management APIs below require:

Authorization: Bearer <access_token>

Keep this management session server-side only. Do not expose it to browsers or mobile clients.

The create calls in this guide are meant for a fresh local development database. If you run them again and receive 409 Conflict, the record already exists; keep using the existing record or change the example IDs.

Run the API server, then call the built-in Finance demo decision:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/authz/check" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"actor": {
"user_id": "user_alice",
"member_id": "member_finance_reviewer",
"user_member_id": "um_alice_finance_reviewer",
"space_id": "space_acme"
},
"resource_type": "invoice",
"resource_id": "invoice_001",
"action": "approve"
}'

PowerShell:

Terminal window
curl.exe -s -X POST "$env:PLYSTRA_URL/api/v1/authz/check" `
-H "Content-Type: application/json" `
-H "Authorization: Bearer $env:PLYSTRA_ACCESS_TOKEN" `
-d '{
"actor": {
"user_id": "user_alice",
"member_id": "member_finance_reviewer",
"user_member_id": "um_alice_finance_reviewer",
"space_id": "space_acme"
},
"resource_type": "invoice",
"resource_id": "invoice_001",
"action": "approve"
}'

Expected result:

{
"data": {
"decision": "allow",
"deny_code": null
}
}

The real response includes actor, resource, matched permission candidates, scope checks, and audit metadata.

Create the business object type and actions your app wants to protect. This example uses expense_report so it can run alongside the seeded invoice demo data.

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/resource-types" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "rt_expense_report",
"key": "expense_report",
"display_name": "Expense Report",
"description": "Employee expense report",
"status": "active",
"source": "core"
}'
Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/resource-types/expense_report/actions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "ra_expense_report_approve",
"key": "approve",
"display_name": "Approve",
"risk_level": "high",
"audit_default": true
}'

If you use the built-in resources table to mirror external objects, register the internal mapping:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/resource-types/expense_report/mapping" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "rm_expense_report_internal",
"storage_kind": "internal_table",
"table_name": "resources",
"id_field": "id",
"space_field": "space_id",
"group_field": "group_id",
"owner_member_field": "owner_member_id",
"visibility_field": "visibility",
"metadata_field": "metadata",
"status": "active"
}'

Create the tenant or workspace:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "space_contoso",
"name": "Contoso",
"slug": "contoso",
"type": "customer",
"status": "active"
}'

Create an authorization tree:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/groups" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "group_contoso_finance",
"name": "Finance",
"display_name": "Finance",
"path": "finance",
"status": "active"
}'
Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/groups" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "group_contoso_finance_apac",
"parent_group_id": "group_contoso_finance",
"name": "Finance APAC",
"display_name": "Finance APAC",
"path": "finance.apac",
"status": "active"
}'

For group_tree scope, Plystra checks that the target group path is equal to the anchor path or starts with anchor_path + ".". Here, finance.apac is inside finance.

Create the login identity:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "user_docs_alice",
"email": "[email protected]",
"status": "active"
}'

Create the business actor inside the Space:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/members" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "member_docs_finance_reviewer",
"display_name": "Finance Reviewer",
"member_type": "human",
"status": "active"
}'

Connect the User to the Member:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/user-members" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "um_docs_alice_finance_reviewer",
"user_id": "user_docs_alice",
"member_id": "member_docs_finance_reviewer",
"relation_type": "login",
"is_primary": true,
"status": "active"
}'

If your application already owns login, you usually create User for traceability and send the actor tuple from your trusted backend. You do not need to use Plystra’s /auth/login flow unless you want Plystra Core sessions.

Create a permission for expense report approval under a group tree:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/permissions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "perm_expense_report_approve_group_tree",
"resource": "expense_report",
"action": "approve",
"scope": "group_tree",
"description": "Approve expense reports inside the assigned group tree",
"status": "active"
}'

Create a Space-local role:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/roles" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "role_contoso_finance_approver",
"key": "finance_approver",
"name": "Finance Approver",
"description": "Can approve expense reports in the assigned Finance tree",
"status": "active"
}'

Attach the permission to the role:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/role-permissions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "rp_contoso_finance_approver_expense_report_approve",
"role_id": "role_contoso_finance_approver",
"permission_id": "perm_expense_report_approve_group_tree",
"audit_space_id": "space_contoso"
}'

Grant the role to the Member at the Finance group:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/member-roles" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "mr_docs_finance_reviewer_approver_finance",
"member_id": "member_docs_finance_reviewer",
"role_id": "role_contoso_finance_approver",
"scope_anchor_group_id": "group_contoso_finance",
"status": "active"
}'

Mirror the business object you want to protect:

Terminal window
curl -s -X POST "$PLYSTRA_URL/api/v1/spaces/space_contoso/resources" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN" \
-d '{
"id": "expense_report_001",
"resource_type": "expense_report",
"external_id": "er-001",
"display_name": "Expense Report 001",
"group_id": "group_contoso_finance_apac",
"owner_member_id": "member_docs_finance_reviewer",
"visibility": "private",
"status": "active",
"metadata": {
"amount": 1200,
"currency": "USD"
}
}'

Call Plystra before your application performs the business operation.

Node/Express example:

async function requirePlystraAllow(input: {
userId: string;
memberId: string;
userMemberId: string;
spaceId: string;
resourceType: string;
resourceId: string;
action: string;
}) {
const response = await fetch(`${process.env.PLYSTRA_URL}/api/v1/authz/check`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PLYSTRA_ACCESS_TOKEN!}`,
},
body: JSON.stringify({
actor: {
user_id: input.userId,
member_id: input.memberId,
user_member_id: input.userMemberId,
space_id: input.spaceId,
},
resource_type: input.resourceType,
resource_id: input.resourceId,
action: input.action,
}),
});
if (!response.ok) {
throw new Error(`Plystra authz call failed: ${response.status}`);
}
const envelope = await response.json();
const decision = envelope.data;
if (decision.decision !== "allow") {
const denyCode = decision.deny_code ?? "DENIED";
const error = new Error(`Forbidden by Plystra: ${denyCode}`);
(error as any).status = 403;
(error as any).denyCode = denyCode;
throw error;
}
return decision;
}
app.post("/expense-reports/:id/approve", async (req, res) => {
await requirePlystraAllow({
userId: req.user.id,
memberId: req.user.activeMemberId,
userMemberId: req.user.activeUserMemberId,
spaceId: req.user.activeSpaceId,
resourceType: "expense_report",
resourceId: req.params.id,
action: "approve",
});
await approveExpenseReport(req.params.id);
res.status(204).end();
});

Go example:

type AuthzRequest struct {
Actor struct {
UserID string `json:"user_id"`
MemberID string `json:"member_id"`
UserMemberID string `json:"user_member_id"`
SpaceID string `json:"space_id"`
} `json:"actor"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
Action string `json:"action"`
}
type AuthzEnvelope struct {
Data struct {
Decision string `json:"decision"`
DenyCode *string `json:"deny_code"`
} `json:"data"`
}

Send the request to /api/v1/authz/check, require Decision == "allow", and translate any deny into a 403 in your own API.

Create a Legal group and move a test expense report there, or check an expense report whose group_id is outside group_contoso_finance. The same actor should receive:

{
"data": {
"decision": "deny",
"deny_code": "SCOPE_OUT_OF_BOUNDS"
}
}

Common deny codes:

CodeMeaning
USER_MEMBER_REVOKEDThe UserMember binding is not active.
USER_MEMBER_EXPIREDThe binding expired.
CROSS_SPACE_VIOLATIONActor, target, grant, or scope anchor is not in the same Space.
NO_MATCHING_PERMISSIONNo active role permission matches resource/action.
SCOPE_OUT_OF_BOUNDSA matching permission exists but its scope does not cover the target.
GLOBAL_SCOPE_DISABLEDglobal scope is reserved and disabled in v1.0.
INVALID_RESOURCE_TYPEThe resource type is missing from the Resource Registry.
INVALID_RESOURCE_ACTIONThe action is missing for the resource type.

Every authz/check and authz/explain writes a decision trace when audit mode is enabled:

Terminal window
curl -s "$PLYSTRA_URL/api/v1/spaces/space_contoso/audit-logs?resource_type=expense_report&resource_id=expense_report_001" \
-H "Authorization: Bearer $PLYSTRA_ACCESS_TOKEN"

The audit log stores the acting user, member, user-member binding, action, resource, decision, deny code, request ID, server-derived IP and user agent, plus trace JSON.

  • Keep management access tokens server-side. Browser clients should call your backend, not Plystra management APIs directly.
  • Use Plystra User records for traceability even if your main application owns login.
  • Store the active member_id, user_member_id, and space_id in your application session after the user chooses a business identity.
  • Call /api/v1/authz/check before the protected business mutation.
  • Treat deny decisions as normal business outcomes and surface the deny_code in logs.
  • Keep DATA_CONSOLE_ENABLED=false and METRICS_ENABLED=false unless you explicitly need them.
  • In production, configure strong secrets, non-wildcard CORS origins, a public URL, and a real PostgreSQL password.

After this path works, add your own:

NeedPlystra object
Read-only page accesspermission with action read
User-owned recordsself scope and owner_member_id
Department-wide accessgroup scope and group_id
Department plus descendantsgroup_tree scope and scope_anchor_group_id
Tenant-wide operatorspace scope

For exact endpoint groups and response envelopes, continue with HTTP API.