Skip to content

How access works

Every request to your app goes through our edge first. The edge signs the caller in, checks that the app is shared with them, and passes the request on with a signed token that says who they are.

A Worker has no route of its own on the internet, and a container has no public address, so the edge is the only way to reach your app.

Say Grace opens https://hello--acme.jiayang-apps.cloud/report for the first time.

  1. Grace’s browser has no session for this host yet. Because this is a page load, the edge redirects it to the dashboard at https://app.jiayang.cloud/sign-in. It also sets a cookie on the app’s host that remembers /report for ten minutes.

    A request that isn’t a page load, such as a fetch from a script, gets 401 unauthorized instead of a redirect.

  2. Grace signs in to the dashboard with her email address and the six-digit code she gets by email, or with a passkey, Google, Microsoft or her company’s sign-in. If her browser is already signed in to the dashboard, this step is skipped.

  3. The dashboard sends the browser to /.jiayang/session on the app’s host with a one-time code that works for 60 seconds. The edge checks that the code came back to the same browser that left in step 1. It swaps the code for a session on this one host and sets that session as a cookie. The browser goes on to /report.

  4. On this request and every one after it, the edge verifies the session cookie: its signature, its issuer, that it’s for this host, and that it hasn’t expired.

  5. The edge asks our control plane whether Grace can use this app, and with what role. If she can’t, she gets 404 not found and a page titled Nothing to open here, as if there were no app. This is true even if she’s a member of the app’s workspace.

  6. The edge removes every X-Jiayang-* header the browser sent. It then signs a new identity token for Grace, valid for 60 seconds, and adds it as X-Jiayang-Identity, with her email in X-Jiayang-Email.

  7. The edge writes the request to the workspace’s audit log and passes it to your app. If the edge can’t write the log entry, the request doesn’t reach your app.

The edge keeps each app’s session in its own cookie, __Host-jiayang_session. The browser sends it only to that one host, over HTTPS, and page scripts can’t read it.

  • It lasts at most 12 hours, and no longer than the dashboard sign-in it came from.
  • Signing out of the dashboard ends every app session from that sign-in within 60 seconds.
  • The edge removes the cookie from each request before your app gets it, and drops any Set-Cookie from your app that uses the same name.

The edge lets a person in when one of these is true:

  • The app is shared with them, with any role.
  • The app’s visibility is workspace and they are a member of the workspace. They get the viewer role.

If the workspace limits its email domains, the person’s address has to be in one of them too, even when the app is shared with them.

Nobody else gets in. Workspace owners and admins manage every app, but to open a private one they have to share it with themselves first. Workspaces and roles has the details.

Changes to sharing reach the edge within 60 seconds, for connections that are already open too. The edge checks WebSockets, streams and long downloads again while they run, and closes them when access ends or the session expires.

If the edge can’t reach the control plane to ask, it refuses the request with 503.

Public paths are the exception. You open a public path to anyone in the dashboard, so a webhook sender can reach it at https://jiayang-apps.cloud/<workspace>/<app>/<path> without signing in. If the path has a verifier, we check each delivery’s signature and pass it on with a token of kind webhook. Without a verifier, requests arrive with no identity token. See Webhooks and public paths.

Situation Answer
A page load with no session A redirect to sign in
Any other request with no session 401 unauthorized
A return from sign-in with a code that doesn’t work 401 unauthorized, the same at every address, whether or not an app is there. A page load gets a page, Sign in again, or goes on to the app if you’re still signed in to it
Two session cookies for the app’s address 401. A page load gets a page, Signed in twice
A session that has expired or been signed out The cookie is cleared. A page load goes to sign in again, anything else gets 401
Signed in, but the app isn’t shared with you, whether or not you’re in its workspace 404 not found. A page load gets a 404 page, Nothing to open here
Signed in, but there’s no app at this address, or nothing is deployed to it The same 404, and the same page
A POST or other write sent from another site’s page, another app included 403 forbidden
Another site’s page, another app included, opening the app in a frame 403 forbidden
The edge can’t reach our control plane 503 service unavailable. A page load gets a page, This can’t open right now
A Free workspace that has used what its plan includes this month, such as 200,000 requests, twice what Free includes A 402 page, Paused for the month
A Team or Business workspace that has reached its usage limit, or hasn’t paid an invoice during the month A 402 page, Paused at the usage limit
A container app, or an app with key-value storage or a queue, in a workspace on the Free plan A 402 page, This app needs a paid plan
A suspended workspace, or an app we took down A 403 page, App unavailable

An app you can’t open gets the same 404 as an app that doesn’t exist, so nobody can learn which apps a workspace has. That includes its own members. The 402 and 403 pages only go to someone who could otherwise open the app.

One exception: app names are unique in a workspace. A member who tries to make an app with a name that’s taken is told so. They learn that some app has the name, and nothing else about it. A member at the plan’s app limit is told there’s no room for another app, not how many apps there are.

The 403 and 404 pages say which address you’re signed in with. If the app was shared with another of your addresses, sign out in the dashboard and open the link again to sign in as that one.

X-Jiayang-Identity holds a JWT signed with RS256 by a key only the edge has. It lasts 60 seconds and is made out to one app. It says who is calling, their role on this app, and the workspace. The caller is a person’s email or a bypass token’s id.

The public keys are at https://edge.jiayang.cloud/.well-known/jwks.json. We set JIAYANG_APP_ID, JIAYANG_IDENTITY_ISSUER and JIAYANG_JWKS_URL in every app, so the SDKs need no setup. Each SDK checks the signature, the issuer, that the audience is your app, and the expiry. It refuses the request if the token is missing or bad, or if it can’t fetch the keys.

The SDK overview shows the code for each language, every claim in the token, and each check.

The edge removes any X-Jiayang-* header a client sends before it adds its own. Verify the token anyway, and don’t decide anything from X-Jiayang-Email:

  • Your code also runs without the edge in front of it: on your machine, in tests, or behind another proxy. There, a header says whatever the caller wrote.
  • The token is made out to your app and lasts 60 seconds. Every other app refuses a token copied from a request to yours, and after a minute every app refuses it.
  • A request on a public path with no verifier has no identity token, and a verified webhook delivery carries a token of kind webhook. Code that verifies a person’s token refuses both.

Use X-Jiayang-Email to show a name, and verify X-Jiayang-Identity to decide what the caller may do.

A script can’t sign in with a browser, so it can’t use an app’s own address. It calls the app through the apps domain instead, with a bearer token:

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

The token is a bypass token made for that app, or your own CLI session, which is what jiayang curl uses. The request passes the same checks, and your app gets the same identity token, with kind set to service for a bypass token. See Tokens for scripts and machines.

  • It removes the Domain attribute from every cookie your app sets, so a cookie stays on your app’s host and can’t reach another app.
  • It drops any cookie named like one of ours.
  • It marks every response Cache-Control: private, so no shared cache keeps a page meant for one person. A max-age your app sends is kept, so the person’s own browser can still keep hashed files.
  • It adds no-transform to Cache-Control. That asks Cloudflare and any proxy that follows the standard not to rewrite your pages or add scripts to them. The Cloudflare features that don’t read it are switched off for the apps domain. On your own domain your zone’s settings come first if its DNS is on Cloudflare and proxies the record to us. A proxy that ignores it, such as one on a company network, can still change a page. We compress text responses ourselves, with br or gzip, when the browser accepts it and your app didn’t already. A response your app is still writing, such as tokens streamed as they’re made, goes out uncompressed so each part arrives as soon as it’s sent.
  • It adds Content-Security-Policy: frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN, so only your app’s own pages can show it in a frame. Your app’s own policy is kept beside it and can be stricter. See HTTP reference.

For more on how we keep apps apart, see Security.