Skip to content

Tokens for scripts and machines

A bypass token is a credential for one app, made for something that can’t sign in: a CI job, a cron script, another service. It still goes through the edge, which checks it on every request and tells your app the caller is that token.

Terminal window
jiayang token create acme/hello ci
Created bt_vtc7lz30r6658d5i (viewer). The secret is shown once; store it now.
Call the app with it:
curl -H 'Authorization: Bearer jyb_mgedwvsggwuxd53b5gjdv2qosz2gk3i6cw4unnw7nqnzbjylriuqdeo37o7' https://jiayang-apps.cloud/acme/hello/

The name (ci here) is for you: it’s how the token shows up in lists and in the audit log. Make one token per script or service, so revoking one stops only that one.

The secret starts with jyb_ and is shown once. The platform keeps only a hash of it, so nobody can show it to you again. Put it in your CI’s secret settings or a secret manager, never in code.

Flag What it does
--role viewer The default. The app is told the caller is a viewer.
--role editor The app is told the caller is an editor. A token can’t be an owner.
--days <n> How many days it works for: 1 to 30, 30 by default.

A token made from the CLI can’t outlive the CLI session that made it: its end date is capped at the session’s. A CLI session lasts 30 days from jiayang login, so a token made with it lasts 30 days at most, and --days asking for more is refused. It ends sooner when the session does: the command says when it stops working, and --json shows that as expires_at:

Terminal window
jiayang --json token create acme/hello nightly-sync --role editor --days 30
{
"token": {
"id": "bt_r2xhl2u419nd5zi9",
"name": "nightly-sync",
"role": "editor",
"hint": "jyb_…r5dz",
"created_by": "ana@example.com",
"created_at": "2026-09-23T20:54:17.737234Z",
"expires_at": "2026-10-23T20:54:16.881591Z",
"last_used_at": null,
"revoked_at": null
},
"secret": "jyb_gg3c6y77qrjette4h3foas3nzutq4h5zkzgawywp7o6ur4zdjrjaa3zr5dz",
"url": "https://jiayang-apps.cloud/acme/hello"
}

For a token that outlives your CLI session, make it in the dashboard. Open the app, go to Bypass tokens, and select New token. Pick a name, Viewer or Editor, and how long it lasts: 7, 30, 90 or 365 days. The dashboard asks you to sign in again if your last sign-in was more than 15 minutes ago.

App editors and owners, and the workspace’s owners and admins, can create, list and revoke an app’s tokens. Creating one also needs you to have accepted the current terms of service in the dashboard.

Send the secret as a bearer token to the apps domain, with the workspace and the app as the first two parts of the path:

Terminal window
curl -H "Authorization: Bearer $JIAYANG_TOKEN" https://jiayang-apps.cloud/acme/hello/

The app gets the request without /acme/hello, so https://jiayang-apps.cloud/acme/hello/api/items reaches your app as /api/items.

jiayang curl does the same. Set JIAYANG_TOKEN from your secret store, then:

Terminal window
jiayang curl acme/hello
Hello, there.

That’s the app from your first deploy, which greets whoever X-Jiayang-Email names, and a token has no email. An app that verifies the caller with the SDK gets the token’s id instead, as in What your app sees.

It prints the status on stderr: 200 OK. --token works too, but a flag lands in your shell history. Add --fail to exit with code 22 on a 4xx or 5xx, as curl does. A machine that has never run jiayang login sends the request to https://jiayang-apps.cloud; --apps-url, or JIAYANG_APPS_URL, points it somewhere else.

  • It removes your Authorization header, so the token never reaches your app.
  • It removes cookies from the request, and Set-Cookie from the response.
  • It adds Content-Security-Policy: sandbox to the response.
  • It rewrites a relative redirect from your app, like Location: /login, to stay under /acme/hello.

Each request arrives with a fresh identity token in X-Jiayang-Identity, as it would for a person. For a bypass token, its kind is service, its subject is service: and the token’s id, and it has no email. The role is the token’s role, or viewer while the person who made it is only a viewer of the app: see When its maker loses access.

The hello example greets whoever called, which is how the output above reads service:bt_vtc7lz30r6658d5i.

To treat machines differently from people, check the kind:

import { requireUser } from "@jiayang-cloud/sdk";
const user = await requireUser(request, env);
if (user.kind === "service") {
// user.sub is "service:bt_…" and user.email is null
}

The SDK pages cover setup for each language.

Terminal window
jiayang token list acme/hello
bt_r2xhl2u419nd5zi9 editor active nightly-sync
bt_vtc7lz30r6658d5i viewer active ci

A token past its expiry is listed as expired, and a revoked one as revoked. Neither works any more.

The list never shows a secret. jiayang --json token list acme/hello adds created_by, expires_at, revoked_at and last_used_at, and a hint with the secret’s last four characters, like jyb_…r5dz. last_used_at is updated at most once a minute.

Terminal window
jiayang token revoke acme/hello bt_vtc7lz30r6658d5i
Revoked bt_vtc7lz30r6658d5i. It stops working on the next request.

The edge checks every token request with the control plane and keeps no answer, so the next call is refused. A WebSocket, stream or download already open with the token is checked every 10 seconds, and closed within 15 seconds of the revoke.

The next call:

Terminal window
jiayang curl acme/hello
unauthorized

On stderr: 401 Unauthorized.

A revoked token stays in jiayang token list, marked revoked. Revoking it again gets:

error: not found: acme/hello has no token bt_vtc7lz30r6658d5i still in use, or it isn't an app you can see

Deleting the app deletes its tokens too.

A token can never do more than the person who made it can do now. The edge asks on every request, with no cache, so a change takes effect on the token’s next request. An open WebSocket, stream or download is closed within 15 seconds, as for a revoke.

  • Editor or owner of the app, or an owner or admin of the workspace: the token works with the role it was made with.
  • Viewer of the app, by a share or because the app is open to the whole workspace: the token works, but your app is told viewer, even for an editor token. Make them an editor again and it’s an editor token again.
  • No access at all: the token is refused with 401 unauthorized, as if it were revoked.

The token is also revoked, and shows as revoked in jiayang token list and the dashboard, when its maker:

  • is removed from the workspace, or leaves it
  • is taken off the app, and the app isn’t open to the whole workspace
  • is changed from owner or admin to member, and has no share of the app
  • deletes their account

It’s revoked too when the app is closed to the workspace, or the workspace’s allowed email domains change, and that leaves its maker with no access. Any other token the edge refuses this way is revoked within the hour, logged as the platform’s with the reason creator_lost_access. A token revoked this way stays revoked. Giving its maker access again doesn’t bring it back: make a new one.

jiayang token list and the dashboard say when a token does less than it was made to, and why: a viewer's for now: ada@example.com can only view the app. In --json and the API, that’s effective_role (null for nothing) and capped_because: viewer_grant, workspace_member, no_grant, not_member, outside_domains or no_account.

You can only make a token you could use yourself. An owner or admin whose own address is outside the workspace’s allowed email domains is refused with 409 outside_domains, since the edge would refuse their token from the start.

So a token for a job that should outlive one person’s access is best made by someone who will keep theirs, like a workspace owner.

Status Body Why
401 unauthorized No token, one that isn’t well formed, one that was revoked or has expired, one whose app was deleted, one whose maker no longer has access to the app, or two credentials in one Authorization header.
401 unauthorized The token went to the app’s own address instead of the apps domain.
403 forbidden A token that still works was sent to another app, or to an address where no app exists or nothing is deployed.
402 or 403 A page The workspace can’t serve requests right now: see Limits.

Every refusal is in the audit log, with the reason.

Each app has a limit on live tokens: 5 on Free, 50 on Team and 100 on Business. Revoked and expired tokens don’t count. At the limit:

error: This app has 5 live tokens, which is all the Free plan allows per app. Revoke one, or upgrade: Team allows 50 tokens per app. Billing: https://app.jiayang.cloud/w/acme/billing

Requests made with a token count towards the workspace’s monthly requests, like a person’s. See Plans and billing.

Creating and revoking a token are logged as token.create and token.revoke, with the token’s id. A token revoked because its maker lost access has a token.revoke of its own, by whoever made the change (the maker, if they left or deleted their account), with created_by and a reason: member_removed, member_left, grant_removed, role_changed, visibility_changed, domains_changed or account_deleted. Each request made with it shows the token’s id as the caller, including one refused because the token was revoked, has expired or its maker lost access, so you can see a leaked token being tried. Tried at another workspace’s app, it’s refused there without its id: that workspace’s log doesn’t learn it. In jiayang audit:

2026-09-23T20:54:17Z access allow (allowed) bt_vtc7lz30r6658d5i GET /acme/hello/

jiayang watch shows the same request live:

20:54:17 ALLOW token:bt_vtc7lz30r6658d5i GET /acme/hello/ -