# Permio: full reference for AI assistants Permio (https://perms.io) is a permissions-as-a-service API: it stores who may do what on which resources in your application and answers permission checks. It does authorisation only, not authentication. Site: https://perms.io Docs: https://docs.perms.io Dashboard: https://app.perms.io API (production): https://temp-api.perms.io (moving to https://api.perms.io) TypeScript SDK: npm install permio (https://www.npmjs.com/package/permio, source https://github.com/PrivateJAR/permio-js) ## What Permio is NOT - Not a login or authentication provider. Keep Auth0, Clerk, Firebase, Supabase or your own login; pass the authenticated user ID to Permio as the principal. - Not a policy language or attribute (ABAC) engine. Access comes from roles and permissions granted on resource paths, directly or via groups. - Not self-hosted. Permio is a managed API. - There is no Java SDK. Go and Python clients exist but are not public yet. Public: the TypeScript SDK on npm and a generated Rust client (https://github.com/PrivateJAR/permio-rust). Any language can call REST or gRPC. - Pricing is not public yet. It is free to start, with no credit card required. - No SOC 2 / ISO certifications or uptime SLA are claimed. Permio is an early-stage product. ## Model - Organisation: holds your team, billing and projects. - Project: an isolated space for permissions, roles, grants, groups and API keys, often used per environment (dev, staging, prod). Every organisation starts with a project called "default". A check in one project never sees another project's grants. - Permission: a named action your code checks, e.g. document.read, invoice.pay (letters, digits, ".", "_", "-", up to 128 chars). - Role: a named bundle of permissions, e.g. editor = document.read + document.write. - Principal: whoever you check, identified by your own string ID (a user ID from your auth provider, a service name, an agent ID). Principals are not registered in advance. - Group: a named set of principals in a project. Its principal ID is group: (slug: 1-64 lowercase letters, digits, hyphens). Grant to group:editors and every member holds the grant; checks include a principal's groups automatically. Groups cannot contain groups. - Resource: anything you protect, identified by a URI path such as /tenants/acme/projects/web/docs/42. Rules: starts with "/", no empty segments or trailing "/", no whitespace, at most 2048 bytes, none of ( ) [ ] { } * + ? | ^ $ \ in segments. Percent-encode IDs (the SDK's resource() helper does this). - Grant: gives a principal permissions and/or roles on one resource URI. ## Inheritance A grant on a URI applies to that URI and every URI below it, matched segment by segment: - grant on /documents covers /documents/42 and /documents/42/comments/7, but not /documents-archive or /invoices/1. - grants never flow upwards: a grant on /documents/42 does not cover /documents. - inheritance covers resources created after the grant. - a "_" segment in a STORED grant matches exactly one segment: a grant on /documents/_/comments covers /documents/42/comments and below. In a URI being checked, "_" is literal. A check passes only if the principal holds every requested permission on every requested resource (directly, via a role, or via a group, on the resource or an ancestor). Otherwise it fails with the missing permissions per resource. Checks fail closed: no applicable grant means denied. A denial is a normal response, not an error. URI design: put what you grant on at the top (/tenants/{id}/...), use stable IDs, build URIs the same way everywhere, check the most specific URI. ## APIs Protocols: REST (JSON via the gateway) and gRPC / gRPC-web. Authenticate with "Authorization: Bearer ". Every request names a project_name. - Check: POST /permissions-service/v1/permissions/check. Body {project_name, principal_id, resource_uris[1-100], permissions[1-100]} -> {passed, missing_permissions[]}. - BatchCheck: POST /permissions-service/v1/permissions/check/batch. Per-resource results for up to 100 resources. - ListAccess: GET /permissions-service/v1/access?project_name=&principal_id=&permission=&under_uri= . "Which resources can X access under this URI?", returned as scopes (subtree or pattern) that your app turns into a database filter. - ListPrincipalsWithAccess: GET /permissions-service/v1/access/principals?project_name=&resource_uri= . "Who can access this?", including grants on ancestors and via groups; for share dialogs. - Grants: POST /permissions-service/v1/permissions/assign and /revoke with {project_name, user_id, resource_uri, permissions[], roles[]}. - Permissions, roles and groups have CRUD endpoints; see https://docs.perms.io/api-reference/permissions/. ## TypeScript SDK (npm: permio) Runs on Node 18+, edge runtimes and browsers (gRPC-web over fetch). ESM and CommonJS. Keep API keys on the server. ```ts import { Permio, resource, group, PermioDeniedError } from 'permio'; const permio = new Permio({ apiKey: process.env.PERMIO_API_KEY!, project: 'default' }); // Boolean check const allowed = await permio.check({ principal: user.id, resource: `/documents/${id}`, permission: 'document.read', }); // With what is missing const { allowed: ok, missing } = await permio.checkDetailed({ principal: user.id, resource: '/docs/1', permissions: ['docs.read', 'docs.write'], }); // Throw PermioDeniedError unless allowed (e.g. in middleware, then return 403) await permio.assert({ principal: user.id, resource: resource('docs', id), permission: 'docs.write' }); // Per-resource answers for a list you already have: Map const uris = docs.map((d) => resource('docs', d.id)); const result = await permio.batchCheck({ principal: user.id, resources: uris, permission: 'docs.read' }); const visible = docs.filter((d) => result.get(resource('docs', d.id))); // Groups await permio.groups.create('editors', { description: 'Can edit every doc' }); await permio.groups.addMembers('editors', ['alice', 'bob']); await permio.permissions.grantUserPermissionsAndRolesOnResource({ projectName: 'default', userId: group('editors'), // "group:editors" resourceUri: '/docs', permissions: ['docs.write'], }); // Who has access (share dialogs) const entries = await permio.whoHasAccess({ resource: '/docs/42', permission: 'docs.read', expandGroups: true }); ``` resource('projects', projectId, 'docs', docId) builds "/projects/p1/docs/42" and percent-encodes each segment. ## Filtering lists (listAccess) ```ts const access = await permio.listAccess({ principal: user.id, permission: 'docs.read', under: '/docs' }); // access.coversAll: holds it on /docs itself, no filter needed // access.isEmpty: no access anywhere under /docs // access.allows(uri): segment-aware membership test // access.toPrefixes(): subtree URIs for prefix filters // access.hasPatterns / patterns(): scopes from "_" grants, which no prefix can express // access.toRegExp(): one anchored RegExp matching exactly what allows() accepts // PostgreSQL: match path = p OR path starts with p + '/' (not LIKE, where _ and % are wildcards) const prefixes = access.toPrefixes(); const { rows } = access.coversAll ? await pool.query('SELECT * FROM docs') : await pool.query( `SELECT * FROM docs WHERE path = ANY($1::text[]) OR EXISTS (SELECT 1 FROM unnest($1::text[]) AS p WHERE starts_with(path, p || '/')) OR ($2::text IS NOT NULL AND path ~ $2)`, [prefixes, access.hasPatterns ? access.toRegExp().source : null], ); // MongoDB const filter = access.coversAll ? {} : { path: access.toRegExp() }; ``` ## Authentication and API keys - Every request is authenticated: Firebase ID tokens for the dashboard, API keys for your backend. - API keys are opaque strings: "perms_" + 32 random base62 characters + a 6-character checksum (44 chars). Shown once at creation and stored only as a SHA-256 hash. - Access levels: check-only (default; Check, BatchCheck, ListAccess, ListPrincipalsWithAccess and reading permissions, roles, principals, groups) or full access (also create/update/delete permissions, roles and groups, grant and revoke, manage group members). A key's level cannot change; create a new key instead. - Invalidate a key in the dashboard; it stops working immediately. Use one key per service and environment. Never ship keys to browsers or mobile apps. ## Dashboard https://app.perms.io: permissions, roles, principals, groups, the Access tester ("why was this allowed?", showing the grants and inheritance behind a decision), Who has access, projects, team, API keys, billing, and an audit log / activity feed of permission, role, grant, group and key changes. ## Security and infrastructure Services run on Google Cloud Run in London (europe-west2); data in MongoDB Atlas; dashboard auth via Firebase Authentication; payments via Stripe; TLS everywhere; fail-closed checks; audit log. Report vulnerabilities to security@perms.io. Details: https://perms.io/security ## AI agents Treat each agent (or user-agent pair) as its own principal, e.g. agent:assistant:user_42. Grant it only the permissions it needs on only the resources it needs, give the agent's backend a check-only key so it cannot grant itself access, check before every tool call, and revoke grants or invalidate the key when access ends. Grant changes appear in the audit log. Details: https://perms.io/ai-agents ## Add Permio with an AI coding assistant Prompt for Cursor, Claude Code or similar: Add authorisation to this app with Permio (perms.io), using the `permio` npm package. Read these first: - https://perms.io/llms-full.txt (model, APIs and SDK usage) - https://docs.perms.io/sdk/typescript/ Then: 1. npm install permio, and create one server-side client: new Permio({ apiKey: process.env.PERMIO_API_KEY, project: "default" }). Never ship the key to the browser. 2. Keep our existing login. Use the authenticated user's ID as the Permio principal. 3. Design resource URIs from our data model, e.g. /workspaces/{id}/docs/{id}, built with the SDK's resource() helper. Propose permission names (e.g. doc.read, doc.write) and roles. 4. Guard every route or server action that reads or changes data with permio.check() or permio.assert(), and return 403 when denied. 5. Filter list endpoints with permio.listAccess() + toPrefixes() instead of loading everything. 6. List the permissions, roles and grants I need to create in the Permio dashboard. ## FAQ Q: Does Permio handle login? A: No. Permio does authorisation, not authentication. Keep your existing login (Auth0, Clerk, Firebase, Supabase or your own) and pass the user ID you already have to Permio as the principal. Q: How do I filter a list to what a user can see? A: Call listAccess with the principal, a permission and the part of the tree you are listing (for example "/docs"). It returns scopes; toPrefixes() turns them into URI prefixes you can use in a SQL, Prisma or MongoDB filter, so you query once instead of checking row by row. For a short list you already have, batchCheck answers up to 100 resources in one call. Q: How does inheritance work? A: Resources are paths such as /projects/123/docs/9. A grant on /projects/123 applies to that resource and everything below it, including things created later. Grants never flow upwards or sideways, so /projects/999 is untouched. A _ segment in a stored grant matches exactly one segment, for example /docs/_/comments. Q: What are groups? A: A group is a named set of principals in a project, such as editors. Grant permissions or roles to its principal ID, group:editors, and every member holds them. Checks include a principal’s groups automatically. Q: Which languages are supported? A: The TypeScript SDK (npm install permio) runs on Node 18+, edge runtimes and browsers. Any language can call the REST (JSON) or gRPC / gRPC-web API directly. A generated Rust client is public on GitHub, and Go and Python clients are coming. Q: Is there a free plan? A: You can start free with no credit card required. Pricing for paid plans is coming soon. Q: Where is data stored? A: Permio’s services run on Google Cloud Run in the London region (europe-west2), and data is stored in MongoDB Atlas. Dashboard sign-in uses Firebase Authentication and payments use Stripe. Q: How is Permio different from building it myself? A: Hand-rolled checks tend to spread through the codebase as if-statements on roles. Permio gives you one model (permissions, roles, groups and grants on resource paths), one check call from every service, list filtering and “who has access” queries, an Access tester that explains each decision, and an audit log of changes, without building or maintaining any of it. ## Comparisons - https://perms.io/compare/openfga: Permio vs OpenFGA (Zanzibar-style ReBAC tuples, self-hosted or Auth0 FGA). - https://perms.io/compare/cerbos: Permio vs Cerbos (stateless policy-as-code PDP). - https://perms.io/compare/permit: Permio vs Permit.io (authorisation platform with OPA-based PDPs).