Scoped Tokens & Permissions
Arca uses an IAM-style, resource/capabilities permission model inspired by AWS. Both scoped JWT tokens (for end-users) and scoped API keys (for services) carry policy statements that grant specific actions on specific Arca path patterns.
Policy Model
A scope contains one or more policy statements. Each statement has an effect ("Allow" or "Deny"), a set of actions, and a set of resources (Arca object path patterns):
{ "statements": [ { "effect": "Allow", "actions": ["arca:TransferFrom", "arca:ReceiveTo"], "resources": ["/users/u123/*"] }, { "effect": "Allow", "actions": ["arca:Read"], "resources": ["*"] }, { "effect": "Deny", "actions": ["arca:*"], "resources": ["/_internal/*"] } ]}Anything not explicitly allowed is denied by default. Deny statements act as safety rails — they override any Allow statements. Use them to protect sensitive paths even when a broad Allow is in effect.
Action Catalog
Retrieve the full catalog programmatically via GET /api/v1/permissions (no auth required).
Object Lifecycle
| Action | Description | Checked against |
|---|---|---|
arca:CreateObject | Create a new Arca object | Path being created |
arca:DeleteObject | Delete an Arca object | Object being deleted |
Balance (directional)
| Action | Description | Checked against |
|---|---|---|
arca:TransferFrom | Debit funds (source side) | Source path |
arca:ReceiveTo | Receive funds (deposit, transfer credit, or sweep) | Target path |
arca:WithdrawFrom | Initiate outbound withdrawal | Source path |
Read / Observe
| Action | Description |
|---|---|
arca:ReadObject | View object metadata and status |
arca:ReadBalance | View object balances |
arca:ReadOperation | View operations |
arca:ReadEvent | View events |
arca:ReadDelta | View state deltas |
arca:Subscribe | SSE real-time stream |
Convenience Aliases
Aliases expand to individual actions. Named aliases are expanded at mint time (stored in the JWT). The wildcard arca:* is stored as-is and matches any action at check time.
| Alias | Expands to |
|---|---|
arca:Read | All arca:Read* + arca:Subscribe |
arca:Transfer | arca:TransferFrom + arca:ReceiveTo |
arca:Fund | arca:ReceiveTo + arca:WithdrawFrom |
arca:Lifecycle | arca:CreateObject + arca:DeleteObject |
arca:Write | All write actions |
arca:* | Everything (evaluated at check time) |
Resource Patterns
/treasury/usd— exact match/users/*— matches the prefix and all paths below it*— matches all resources
Path safety note: Arca paths have no directory-traversal semantics. The segments . and .. are treated as literal characters. There is no path normalization — what you see is what matches. This is a deliberate security invariant.
Authorization Principles
- Deny by default. Anything not explicitly granted by an Allow statement is denied.
- Explicit Deny overrides Allow. If any Deny statement matches an (action, resource) pair, the request is blocked — regardless of any Allow statements. Use Deny as a safety rail to protect sensitive paths (e.g.,
/_internal/*). - Actions are directional and per-resource. For operations touching multiple objects, each side is checked independently. A transfer checks
arca:TransferFromon the source andarca:ReceiveToon the target. - Inbound permissions are unified.
arca:ReceiveTocovers deposits, transfer credits, and sweeps. The balance outcome is identical regardless of channel. Outbound actions (arca:TransferFrom,arca:WithdrawFrom) remain separate because they cross different trust boundaries. - Delete + sweep is compound authorization. Deleting an object with balance requires
arca:DeleteObjecton the source andarca:ReceiveToon the sweep target. Without a sweep target,arca:DeleteObjectalone suffices for zero-balance objects. - Realm is a boundary, not a resource. Scoped tokens are locked to one realm. Resource patterns do not encode realm — this simplifies matching.
Mint Scoped Token
POST/api/v1/auth/tokenJWT / API Key
Mint a scoped JWT bound to a single realm with IAM-style policy statements. The resulting token is passed from the builder's backend to the end-user's frontend.
Request Body
realmIdstringrequiredsubstringrequiredscopeobjectrequiredstatements array. Each statement has effect ("Allow" or "Deny", default "Allow"), actions (action strings or aliases), and resources (path patterns).expirationMinutesnumber60.Response 201 Created
{ "success": true, "data": { "token": "eyJhbGciOiJIUzI1NiIs...", "expiresAt": "2026-02-14T15:00:00Z" }}Example
# Builder's backend mints a token for user "alice"curl -X POST http://localhost:8080/api/v1/auth/token \ -H "Authorization: Bearer arca_78ae7276_..." \ -H "Content-Type: application/json" \ -d '{ "realmId": "6d25623e-...", "sub": "alice", "scope": { "statements": [ { "effect": "Allow", "actions": ["arca:Read"], "resources": ["*"] }, { "effect": "Allow", "actions": ["arca:TransferFrom", "arca:ReceiveTo"], "resources": ["/users/alice/*"] }, { "effect": "Deny", "actions": ["arca:*"], "resources": ["/_internal/*"] } ] }, "expirationMinutes": 30 }'Permissions Catalog
GET/api/v1/permissionsNone
Returns the full action catalog with descriptions, grouped by category, plus all available aliases. No authentication required.
Auth Audit Log
GET/api/v1/auth/auditJWT / API Key
Returns a paginated list of authentication and authorization events for the authenticated builder. Tracks sign-ins, token minting, API key usage, and permission denials.
Query Parameters
eventTypestringsign_in, token_minted, api_key_authenticated, permission_denied.realmIdstringsincestringuntilstringsuccessbooleantrue for successful events, false for denials.limitnumberoffsetnumberResponse 200 OK
{ "success": true, "data": { "entries": [ { "id": "...", "builderId": "...", "eventType": "token_minted", "actorType": "builder", "actorId": "...", "realmId": "...", "subject": "alice", "scopeSummary": "{...}", "tokenJti": "...", "expiresAt": "2026-02-21T15:00:00Z", "success": true, "operationCount": 12, "createdAt": "2026-02-21T14:00:00Z" } ], "total": 42 }}Scoped tokens can access this endpoint with the arca:ReadAuditLog action (included in the arca:Read alias). The operationCount field on token_minted entries shows how many operations were triggered by that token's subject.
Credential Scope Lookup
GET/api/v1/auth/audit/scopeJWT / API Key
Returns the permissions (scope) of a specific credential — either a scoped JWT (by tokenJti) or an API key (by apiKeyId). Use this to inspect what permissions a credential carried when it was used to perform operations.
Query Parameters
tokenJtistringtoken_minted audit entry to retrieve the scope.apiKeyIdstringapi_keys table.Response 200 OK
{ "success": true, "data": { "credentialType": "scoped_token", "credentialId": "abc-123-jti", "subject": "alice", "scope": "{\"statements\":[...]}", "fullAccess": false, "createdAt": "2026-02-21T14:00:00Z", "expiresAt": "2026-02-21T15:00:00Z" }}Requires the arca:ReadAuditLog permission. For API keys without a scope, fullAccess is true and scope is null.
Scope Enforcement
When a scoped token (or scoped API key) is used, the API enforces:
- Realm lock: Scoped tokens can only access the realm specified at mint time. Requests to other realms return
403 FORBIDDEN. - Policy evaluation: Each operation produces one or more (action, resource) pairs. For each pair: (a) if any Deny statement matches, access is blocked; (b) if any Allow statement matches, access is granted; (c) otherwise, access is denied (implicit deny). All pairs must pass for the request to succeed.
- Resource matching: Requested paths must match at least one pattern in the granting statement. Patterns ending in
/*match all descendants.
Integration Flow
BuilderBackend ArcaAPI EndUserFrontend | | | |--- POST /auth/token --->| | |<-- scoped JWT ----------| | | | | |--- hand JWT to user --->| | | | | | |<-- POST /transfer -| | | (Bearer scoped) | | |-- verify + enforce -| | | | | |--- response ------>|Security Pre-flight Checklist
When exposing Arca to end-users via scoped tokens, the following items remain in the builder's domain. Arca enforces token scope at the API layer, but the builder is responsible for minting correct tokens in the first place.
Each item below represents a security boundary that Arca cannot yet make completely foolproof. Address all of them before shipping scoped-token access to production.
- Never expose API keys on the frontend. API keys are team-scoped with full access to every realm. Always use scoped tokens for end-user-facing code. If an API key leaks, revoke it immediately from the API Keys page.
- Scope tokens to the user's Arca paths. When minting a token, restrict
pathsto the current user's subtree (e.g.,/users/{userId}/*). A token withpaths: ["/*"]gives access to every object in the realm. - Grant minimal permissions. Only include the permissions the user actually needs. Most end users need
readandtransfer— notdeleteorcreate. - Set short expiry times. Frontend tokens should expire in 15–60 minutes. The builder's backend should handle refresh by minting a new token when the current one is close to expiry.
- Validate scope server-side before minting. The builder's backend must verify that the requesting user is entitled to the paths and permissions in the token. Arca trusts the builder to get this right — it has no knowledge of the builder's user model.
- Do not let the frontend request its own scope. The end-user's frontend should not send the desired scope to the builder's backend. The backend should derive the scope from the user's authenticated identity and business rules.
- Use distinct realms for dev and production. Demo realm data is simulated and has permissive defaults. Never use a demo realm for real transactions.
- Treat operation paths as idempotency keys. If a transfer path is reused, the original result is returned (no double-spend). Build unique paths — include a nonce or UUID (e.g.,
/op/transfer/{).userId}-{ uuid} - Monitor via events. Subscribe to realm events (SSE or polling) to detect unexpected activity — transfers you did not initiate, objects created outside your expected paths, etc.
Error Handling
Errors follow a consistent structure with a machine-readable code and a human-readable message.
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Input validation failed |
UNAUTHORIZED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token scope does not permit this action |
SIGNUP_FAILED | 400 / 409 | Sign-up failed (invalid input or duplicate email) |
SIGNIN_FAILED | 401 | Invalid email or password |
NOT_FOUND | 404 | Resource not found or not accessible |
DUPLICATE_REALM | 409 | A realm with this slug already exists |
ALREADY_REVOKED | 409 | API key is already revoked |
CONFLICT | 409 | Duplicate resource or invalid state transition (explorer) |
INTERNAL_ERROR | 500 | Unexpected server error |
Example Error Response
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Realm name must be 100 characters or fewer" }}