Skip to content

Security

Your app is reachable only through the platform’s edge, which signs people in, checks who may use the app, and records every request. This page says what the edge does, what it leaves to your code, and how to report a problem.

Your app has no address of its own. A JavaScript app runs as a Worker with no routes. A container app has no public ingress. Every request for every app goes through the edge first.

Apps answer at https://<app>--<workspace>.jiayang-apps.cloud, for people in a browser. Scripts and machines call https://jiayang-apps.cloud/<workspace>/<app>/ with a bypass token. Both are the edge.

For each request, the edge does these in order, and denies the request at the first step that fails:

  1. Works out who is calling: the app host’s session cookie, or a bearer token.
  2. Refuses a write sent from another site.
  3. Asks whether that caller may use this app.
  4. Deletes every identity header the client sent.
  5. Signs a fresh identity token for your app.
  6. Records the request in the audit log.
  7. Passes the request to your app.

A denied request is recorded too. Your app never sees it.

A person signs in once, on the dashboard. The first time they open an app, the edge sends them there and back, and sets a session cookie for that app’s host alone.

The cookie is __Host-jiayang_session: HttpOnly, SameSite=Lax, Secure, and valid for one host. It lasts as long as the dashboard session behind it, 12 hours at most. The edge checks its signature, issuer, audience and expiry on every request.

Your app never sees that cookie. The edge takes it out of the Cookie header before passing the request on, and drops any Set-Cookie from your app that uses one of the platform’s cookie names.

A browser write to your app (a POST, PUT, PATCH, DELETE or WebSocket) that comes from another site gets 403, even with a valid session. Apps on the platform share a parent domain, so this is what stops one app’s page from acting on another as its visitor.

For every request it lets through, the edge signs a new token for your app and sends it in X-Jiayang-Identity. It is an RS256 JWT, made out to your app alone, and it lasts 60 seconds.

Your app has to verify it. Check the signature against the key set at https://edge.jiayang.cloud/.well-known/jwks.json, then iss, aud and exp. The SDKs do all of it in one call, and refuse the request if anything fails, including a key set they couldn’t fetch.

Before the edge adds its own headers, it deletes every header the client sent whose name starts with X-Jiayang-, Cf-Access- or Cf-Container-, with dashes or underscores. It then sets two:

  • X-Jiayang-Identity, the signed token
  • X-Jiayang-Email, the caller’s email, for display only

The caller’s IP address is in X-Forwarded-For, and it’s the only address there: the edge writes it from the address Cloudflare saw the request come from, and writes X-Forwarded-Proto too. It removes whatever the client sent in those, and in Forwarded, X-Forwarded-Host, X-Real-IP, True-Client-IP and the other headers a proxy uses to say who called, so a client can’t pick the address your app logs or rate-limits.

A webhook delivery on a verified public path gets only X-Jiayang-Identity. Its token is of kind webhook and made out to webhook:<app id>, so a check for a person refuses it. See Public paths and webhooks.

The presence of a header proves nothing. Authorize on the verified token, never on X-Jiayang-Email. The SDKs read only the token.

  • Taking someone’s access away ends it within 60 seconds. From their first refused request on, every request they send through that Cloudflare location is refused too.
  • Signing out of the dashboard ends that browser’s sessions on every app host within 60 seconds.
  • A revoked bypass token stops working on its next request. The edge checks bearer tokens with the control plane on every request, and never caches them.
  • A bypass token can never do more than the person who made it can do now. Once they have no access to its app (removed from the workspace, taken off the app, or their account deleted), it’s refused on its next request and marked revoked. While they’re only a viewer, it’s a viewer’s token. See When its maker loses access.

Open connections end too. The edge keeps checking access while a WebSocket, a streamed response or a long download is open. It closes the connection within 60 seconds of access being taken away, and when the session behind it ends. A connection opened with a bypass token or a CLI session is checked every 10 seconds instead, and closes within 15 seconds. A slow app isn’t cut off for being slow: if its answer starts after the last check has run out, the edge checks again before sending it, and sends it only if access still holds.

When the edge can’t decide, it refuses:

  • If it can’t reach the control plane to check access, it answers 503.
  • If it can’t get a clear yes on a webhook delivery’s signature, it answers 503, which the provider sees as a failed delivery to retry. Only an explicit allow reaches your app.
  • If it can’t record an allowed request in the audit log, it answers 503 and your app never sees the request.
  • A request with two session cookies, or two bearer tokens, gets 401. The edge doesn’t guess which one was meant.

The SDKs do the same in your app. Missing variables or an unreachable key set mean every request is refused.

  • Set-Cookie headers lose their Domain attribute, so your app can’t set a cookie for the other apps on the platform.
  • Every response gets Cache-Control: private, and headers that tell a CDN to cache it are removed. Only the caller’s own browser may keep a copy, for as long as your app’s max-age says.
  • Over HTTPS, every response carries Strict-Transport-Security: max-age=63072000; includeSubDomains.
  • Responses to scripts at jiayang-apps.cloud/<workspace>/<app>/ carry Content-Security-Policy: sandbox and can’t set cookies, since every app answers on that one host there.

Your app holds no third-party credentials. You store one with jiayang secret set, naming the one host it’s for and the header it goes in. When your app makes an HTTPS request to that host, the platform adds the header on the way out. The value isn’t in your code, its environment, its logs or its versions, and nobody can read it back from the platform: a listing shows each secret’s name and where it’s sent, never its value. See Secrets and outbound calls.

This keeps the value out of your app, but it can’t stop the host you named from handing it back. If that host puts request headers in a response, as a debugging endpoint that echoes requests does, or quotes the key in an error message, your code reads the value there like any other response. Anyone who can deploy the app can write code that calls such an endpoint. Name only a host you trust with the key, and treat everyone who can deploy the app as able to use it.

A secret is only added over HTTPS, and a redirect from that host is never followed with it.

A webhook’s signing secret stays out too. You paste it into a public path’s verifier in the dashboard, and the platform checks each delivery with it before your app sees the delivery. It’s stored the same way as a secret, and opened only to check a delivery to that path.

The platform also stops credentials going where they don’t belong:

  • jiayang deploy refuses to upload a .env file, a private key or anything else that looks like a credential.
  • jiayang env set refuses a variable whose name or value looks like one, and points you to jiayang secret set, or to a verifier for a webhook’s signing secret.

Your app’s outbound requests can’t reach the platform’s own hosts: the dashboard, the API, the edge or another app. Such a request gets 403 egress to platform hosts is blocked. The one exception is the key set your app verifies tokens with.

A public path is the one way into your app without signing in. It exists for webhooks.

  • On a path with a verifier, only a delivery whose signature holds reaches your app, with a token of kind webhook that requireWebhook checks and requireUser refuses. Every failed check gets the same 401, so a stranger can’t tell which one failed.
  • To check a delivery, the edge sends its body and signature headers to the platform’s control plane before your app sees it. Nothing keeps the body. The audit log keeps the provider’s id for the delivery where it signs one, and for providers that sign a time a hash of the signature is kept for up to half an hour, so a copy of the delivery can be refused.
  • A copy of a delivery your app answered with a 2xx is refused while it’s still fresh, on Stripe, Slack and Standard Webhooks. GitHub, Shopify and plain HMAC senders sign no time, so your app has to make a repeat harmless itself.
  • On a path with no verifier, a request reaches your app with no identity token, so requireUser still refuses it. Your app has to check the sender’s signature itself.
  • A request that carries a bypass token or a CLI session is that caller’s on a public path too: the edge checks the token as it would anywhere, and refuses one that may not use the app.
  • The edge limits how many requests one client can send an app’s public paths each minute, and answers 429 past that. Verified paths aren’t under that limit: the platform shares out its checks per path and per workspace instead.
  • A public path takes no WebSockets.
  • Only a person in the dashboard can open one or change its verifier. The CLI and agents can list them, and can’t add them or touch a verifier.

Every table in the platform’s database that holds workspace data has a Postgres row-level security policy, so a query can’t return another workspace’s rows. Each app runs in its own isolate or container, with its own storage, secrets and logs. Tests that check one workspace can’t reach another’s database, storage, secrets or logs run on every change to the platform.

  • Verify the identity token on every request. Use an SDK, and never trust a header on its own.
  • Answer 401 when there is no valid caller, and 403 when the caller may not do this.
  • Keep credentials out of your code, your files and your environment. Use jiayang secret set.
  • Treat a public path as open to anyone. Give it a verifier and check the delivery with requireWebhook, or check the signature yourself on a path with none.
  • Treat a bypass token as a secret. Make one per script, and revoke it when the script is retired.

Email security@jiayang.cloud with what you found, how to reproduce it, and what it lets someone do. Don’t open a public issue.

Test only against your own workspaces and apps. The security page has the full policy, including how quickly we answer.