App access control for agents
This reference extends https://app.tokay.io/llms.txt with app access operations for AI agents. Change access only when the user explicitly asks or a named readiness action requires it.
Last updated
AI agent? Start with llms.txt.
The access model
- New apps are public by default so the first result is shareable. A public project makes every WEB and FUNCTION anonymous. A private project applies Tokay Access unless that service or WEB path is a saved public exception. “Private” means Tokay Access protects the public URL. It does not limit the app to a private network.
- The audience is additive. The policy flags admit the owner, project members, and workspace members, and on top of that you can add individual visitors by email and whole email domains. Humans sign in through an emailed magic link on the access page. Programs use a Machine credential when calling as themselves, or Tokay Access OAuth when calling for an admitted person.
- Every protected service shares that one project audience. WEB and FUNCTION may each be public as a whole. Only WEB supports exact public paths or public paths ending in a wildcard. Public Project mode preserves these service, path, and audience settings, then restores them unchanged when the Project returns to private.
- When Tokay authentication is available, access changes reach an active Project in about 10 seconds and an idle Project in up to five minutes. Verify with a real request after a change instead of assuming propagation has finished. During an authentication outage, cached Machine authority can stay usable for up to 48 hours. Human access is limited by the 15-minute token lifetime.
- The canonical
service.publicUrlnever changes with any of this. FUNCTION keeps only its hard to guessfn-*Tokay URL. Only WEB supports custom domains. The URL is hard to guess for routing purposes, not as a form of authentication.
What to call
| To | Use |
|---|---|
| Read the policy | project.projectAccessPolicy |
| Make private or public | updateProjectAccessPolicy |
| Make one WEB/FUNCTION public or restricted | setServicePublicAccess |
| Admit one outside person | addProjectVisitor / removeProjectVisitor |
| Admit a whole email domain | addProjectAccessEmailDomain / removeProjectAccessEmailDomain |
| Open one WEB path publicly | addServicePublicEndpoint / removeServicePublicEndpoint |
| Create a program identity | createMachine |
| Issue or rotate its secret | issueMachineCredential / revokeMachineCredential |
| End the identity | removeMachine |
| See who got in or was denied | projectAccessEvents |
Read and flip the policy
query Policy($projectId: ID!) {
project(id: $projectId) {
projectAccessPolicy {
id isPublic allowOwner allowProjectMembers allowWorkspaceMembers
accessPageLogoUrl accessPageMessage
}
}
}
mutation Private($id: ID!) {
updateProjectAccessPolicy(input: { id: $id, patch: { isPublic: false } }) {
projectAccessPolicy { isPublic }
}
}
Flip the policy only on an explicit user request. Never make an app private, or public, unprompted. The patch takes any of the audience flags plus the bounded access page branding fields. Changing isPublic does not clear the saved audience, public exceptions for whole Services, or WEB paths. After flipping, poll the public URL until it has the intended result before reporting success. An active Project usually converges in about 10 seconds. An idle Project can take up to five minutes.
Admit people
mutation Visitor($project: ID!) {
addProjectVisitor(input: { project: $project, email: "friend@example.com", displayName: "Friend" }) {
projectAccessVisitor { id projectAccessStatus projectAccessSource }
}
}
A new visitor starts as projectAccessStatus: PENDING and becomes active when they first sign in through the magic link. addProjectAccessEmailDomain(input: { project: $project, domain: "example.com" }) admits everyone with a verified address on that domain. Project members are already admitted by the policy flag and never need a visitor row.
Set one service's access
mutation ServiceAccess($service: ID!, $isPublic: Boolean!) {
setServicePublicAccess(input: { service: $service, isPublic: $isPublic }) {
service { id publicAccessOverride effectiveIsPublic }
}
}
The saved override controls whether one WEB or FUNCTION is public when its project is private. While the project is public, effectiveIsPublic is true. The override takes effect if the project becomes private. Making the service public revokes its Tokay Access OAuth grants. Changing it back to restricted requires each client to authorize again. Through official Tokay MCP, use the reviewed set_service_public_access tool rather than generic GraphQL.
Open a public WEB path
mutation Endpoint($service: ID!) {
addServicePublicEndpoint(input: { service: $service, pathPattern: "/webhooks/stripe" }) {
servicePublicEndpoint { id pathPattern }
}
}
Patterns come in two forms. An exact path such as /healthz matches both /healthz and /healthz/. A wildcard at the end of a path, such as /api/*, matches paths beneath /api/, but not bare /api. When the whole surface under a path plus the path itself must be public, add both rules. A wildcard is valid only at the end of the pattern. /* is invalid. Make the whole Service public instead.
These mutations accept WEB services only. Do not try to create a public path exception for a FUNCTION.
Machines and credentials
mutation Machine($project: ID!) {
createMachine(input: { project: $project, name: "ci-reporter" }) {
machine { id principal { displayName } }
}
}
Creating a Machine establishes a durable actor for one Project but emits no secret. Issue an independently named credential next.
mutation Credential($machine: ID!) {
issueMachineCredential(input: { machine: $machine, name: "production" }) {
machineCredentialCreation { machineId keyId name token }
}
}
The token value is shown exactly once. Deliver it to the consuming system immediately and never write it into source or logs. The caller presents it as Authorization: Bearer <token> against the app's own URL. Wait for the Project's access state to reach its sidecar before judging the first call. An active Project usually converges in about 10 seconds, and an idle Project can take up to five minutes.
A Machine may hold several credentials, so rotation is issue new, switch the caller, then revokeMachineCredential. Revoking one credential leaves the durable Machine identity, access history, and other credentials intact. removeMachine ends the identity and revokes all its credentials. A Project may have 100 active Machines and each Machine may have 100 active credentials.
Call a private app for a person
Tokay Access OAuth lets an outside client, such as an MCP client, call a private WEB or FUNCTION app for a person who already has access. A program that calls the app without credentials gets a Bearer challenge naming the exact resource to authorize. For an MCP endpoint at /mcp, discovery starts here.
GET https://your-app.tokay.app/.well-known/oauth-protected-resource/mcp
The client runs the authorization code flow with S256 PKCE and the person approves in the browser. Tokay then issues a token that lasts fifteen minutes and stays bound to that Service and exact resource URI. A root resource covers the whole Service. A resource below the root covers its path and descendants across a / boundary (/api covers /api/x, never /apix). A client that declares refresh support or requests offline_access can also receive a rotating refresh token.
This is Tokay Access OAuth, not Platform OAuth. An app token cannot call Tokay GraphQL, API REST, or the official Tokay MCP, and a Platform token cannot open the app. The two never mix.
Tokay Access OAuth exists only while both Project and Service are restricted. Making the Project public revokes every Project grant. Making one Service public revokes that Service's grants. Saved policy is reversible, issued OAuth authorization is not.
See who got in
query Events($first: Int = 20) {
projectAccessEvents(first: $first) {
nodes { projectAccessActor projectAccessSource projectAccessEvent path requestHost occurredAt }
}
}
The access log records who signed in, who was denied, and which tokens called, with the actor kind and source for each. Use it to answer "who opened this" and to verify that a token or visitor you just configured actually worked.
Error shapes
| You see | It means | Do this |
|---|---|---|
| App URL still answers 200 right after going private | Config propagation is in flight | Poll until the active or idle propagation window has passed before judging |
| 401 from the app with a valid Machine credential | Access state is still propagating, or the credential was revoked | Retry through the expected propagation window, then check the Machine and credential remain active |
OAuth client receives 401 invalid_token |
Its app token expired, was revoked, or the request is outside its bound resource subtree | Refresh if possible or authorize the intended app resource again |
401 on a path you opened with /x/* |
The wildcard does not match the bare prefix | Add the exact /x rule too |
/* is rejected |
Public access for a whole Service has its own setting | Use setServicePublicAccess or set_service_public_access |
| "Not authorized for PROJECT_MANAGE_ACCESS on PROJECT id" | Your credential cannot change access | Report the missing grant to the user |