{"service":"fullstacked-cloud-docs","version":"1.22.3","environment":"live","generatedAt":"2026-10-24T09:42:32.705Z","formats":{"html":"https://docs.fullstacked.cloud/","markdown":"https://docs.fullstacked.cloud/?format=md","json":"https://docs.fullstacked.cloud/?format=json"},"services":{"console":"https://console.fullstacked.cloud","auth":"https://auth.fullstacked.cloud","git":"https://git.fullstacked.cloud","tunnels":"https://tunnels.fullstacked.cloud","tunnelsHost":"tunnels.fullstacked.cloud","mcp":"https://mcp.fullstacked.cloud","install":"https://install.fullstacked.cloud","docs":"https://docs.fullstacked.cloud"},"runtime":{"version":"1.0.0-alpha.1844","guide":"https://docs.fullstacked.cloud/runtime?format=md"},"dataEngineLimits":{"postgres":{"image":"postgres:18.6-alpine","port":5432,"memoryMB":256,"cpus":2,"pids":256},"redis":{"image":"redis:8.10.2-alpine","port":6379,"memoryMB":256,"cpus":1,"pids":128},"mongodb":{"image":"mongo:8.3.11-noble","port":27017,"memoryMB":512,"cpus":2,"pids":512},"s3":{"image":"rustfs/rustfs:1.0.0","port":9000,"memoryMB":768,"cpus":2,"pids":1024}},"projectRuntimeEnv":[{"name":"TUNNELS_HOST","injectedBy":"GET /api/v1/projects/:id/env","description":"Tunnels hub host for tunnel.register from fullstacked/tunnel."},{"name":"AUTH_HOST","injectedBy":"GET /api/v1/projects/:id/env","description":"Bare host of the Authentication service."},{"name":"AUTH_URL","injectedBy":"GET /api/v1/projects/:id/env","description":"Base URL of the Authentication service (GET ${AUTH_URL}/api/v1/me)."},{"name":"DOCS_URL","injectedBy":"GET /api/v1/projects/:id/env","description":"Base URL of this documentation (?format=md | ?format=json)."},{"name":"AUTH_TOKEN","injectedBy":"Client GUI / action runner at launch (in memory, never stored)","description":"The signed-in user's fsc_* bearer token: Authorization: Bearer ${AUTH_TOKEN}."}],"topics":[{"slug":"auth-byoa","title":"Runtime Sign-In & BYOA","url":"https://docs.fullstacked.cloud/auth-byoa","markdownUrl":"https://docs.fullstacked.cloud/auth-byoa?format=md","jsonUrl":"https://docs.fullstacked.cloud/auth-byoa?format=json","markdown":"# Runtime Sign-In & BYOA\n\nProjects launched from the FullStacked Cloud Client GUI already receive the user's token as `process.env.AUTH_TOKEN`: most projects never need to sign anyone in. This page covers the two other cases: a standalone runtime app that signs a user in itself (PKCE through `fullstacked/auth` and the auth page at https://auth.fullstacked.cloud), and an enterprise backend that validates the FullStacked tokens of users signed in through its own identity provider (Bring Your Own Auth).\n\n## Interactive sign-in from a runtime app\n\n`fullstacked/auth` exports one function:\n\n```ts\nauthenticate(url: string, opts?: { windowFeatures?: string }): Promise<any>\n```\n\nIt adds `auth=1` to `url`, opens it in the platform's auth surface (system auth session on Apple and Android, an auth window or browser popup elsewhere) and resolves with the object the page posts back: `{ exchange_token }` on success. A denial resolves `{ error }` (Android rejects with an `Error` instead). Dismissing the auth surface rejects on Apple, Android and Linux; on Windows and in the browser window of the npm CLI the promise may never settle, so keep a way to cancel in your UI. There is no `authorize` export in the runtime.\n\nFlow:\n\n1. Create a PKCE pair: `code_verifier` (32 random bytes, base64url) and `code_challenge` = base64url(SHA-256(verifier)).\n2. ``authenticate(`${AUTH_URL}/?code_challenge=<challenge>&code_challenge_method=S256`)``. The user signs in and approves on the consent screen.\n3. Redeem: `POST ${AUTH_URL}/v1/auth/exchange/token` with `{ exchangeToken, codeVerifier }`. The response carries the session token.\n\n```ts\nimport { authenticate } from \"fullstacked/auth\";\nimport { createHash, randomBytes } from \"node:crypto\";\n\nexport async function signIn(authUrl: string): Promise<string> {\n    const verifier = randomBytes(32).toString(\"base64url\");\n    const challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n\n    const result = await authenticate(\n        `${authUrl}/?code_challenge=${challenge}&code_challenge_method=S256`\n    );\n    if (result?.error) throw new Error(result.error);\n    if (!result?.exchange_token?.startsWith(\"fsc_exc_\")) throw new Error(\"Invalid authentication response\");\n\n    const res = await fetch(`${authUrl}/v1/auth/exchange/token`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ exchangeToken: result.exchange_token, codeVerifier: verifier }),\n    });\n    if (!res.ok) throw new Error(`Exchange failed (${res.status}): ${await res.text()}`);\n    const session = await res.json();\n    return session.token; // fsc_ses_... (or fsc_ath_<slug>_... from an enterprise authority)\n}\n```\n\nCall `signIn` from a click handler so the window can open. Use node `crypto` (core-backed), not `crypto.subtle`.\n\nExchange response (`200`):\n\n```json\n{\n  \"token\": \"fsc_ses_...\",\n  \"tokenType\": \"Bearer\",\n  \"userId\": \"usr_4f1c9a2b7d3e8f6a5b0c1d2e\",\n  \"sessionId\": \"tok_...\",\n  \"user\": { \"userId\": \"usr_4f1c9a2b7d3e8f6a5b0c1d2e\", \"email\": \"alice@example.com\", \"name\": \"Alice Martin\", \"slug\": \"alice\", \"createdAt\": \"2026-09-20T14:32:00.000Z\" },\n  \"session\": { \"sessionId\": \"tok_...\", \"userId\": \"usr_4f1c9a2b7d3e8f6a5b0c1d2e\", \"expiresAt\": 1792000000000 },\n  \"issuedAt\": 1790000000,\n  \"expiresAt\": 1792000000,\n  \"restricted\": false\n}\n```\n\nRules:\n\n- The challenge is mandatory and must be S256 (43 base64url characters); without it the auth page refuses to start.\n- The exchange token (`fsc_exc_...`) is single use and expires after 300 s. A wrong or missing verifier burns it (`400` / `401`). The endpoint is limited to 30 requests per minute per IP (`429 RATE_LIMITED`).\n- `body` keys also accept `exchange_token` / `code_verifier`.\n- Sign out with `POST ${AUTH_URL}/v1/auth/logout` and `Authorization: Bearer <token>` (returns `{ \"status\": \"revoked\" }`).\n- Store the token like a password (for example `fullstacked/config` is plain text in the project directory: acceptable on the user's own device only).\n\n## Enterprise Bring Your Own Auth (BYOA)\n\nAn organization can run its own sign-in for FullStacked users through a hosted **authority** (`ath_<slug>`, created by FullStacked Cloud operators, with a secret key `sec_auth_<env>_...` shown once). Its users get session tokens `fsc_ath_<slug>_<64 hex>` that work like any FullStacked token (`/api/v1/me`, projects, data, tunnels). On `/api/v1/me` their `id` is `<authorityId>:<externalUserId>` and `authority_id` is set.\n\n### Minting a sign-in for your user (your IdP backend)\n\nAfter your identity provider authenticates the user, your backend mints an exchange token bound to the runtime's PKCE challenge and hands it back the same way the FullStacked auth page does. The runtime app opens your sign-in page with `authenticate(\"<your page>?code_challenge=<challenge>&code_challenge_method=S256\")`: the page receives `code_challenge` (the S256 challenge to mint with) and `auth=1`, and native shells also add `native=1` and `callback_scheme=fullstacked-auth`.\n\n- Native (`native=1`): redirect to `<callback_scheme>://auth?exchange_token=fsc_exc_...` (on denial `?error=<message>`), that is `fullstacked-auth://auth?...`.\n- Popup (no `native`): `window.opener.postMessage({ exchange_token }, <opener origin>)` (on denial `{ error }`), then close the window.\n\nThe runtime app then redeems it at `https://auth.fullstacked.cloud/v1/auth/exchange/token` exactly as above and receives `fsc_ath_<slug>_...`.\n\n```http\nPOST /v1/authorities/ath_acme/token HTTP/1.1\nHost: auth.fullstacked.cloud\nX-FullStacked-Authority-Key: sec_auth_...\nContent-Type: application/json\n\n{ \"userId\": \"alice\", \"codeChallenge\": \"<S256 challenge from the runtime>\", \"email\": \"alice@example.com\", \"name\": \"Alice Martin\", \"groups\": [\"devops\"] }\n```\n\nResponse: `{ \"exchangeToken\": \"fsc_exc_...\", \"tokenType\": \"ExchangeToken\", \"authorityId\": \"ath_acme\", \"userId\": \"alice\", \"expiresAt\": ... }` (single use, 300 s). Optional body fields: `ttl`, `metadata`. Limit: 50 mints per second per authority.\n\n### Validating a user's token in your backend\n\nWhen a project calls one of your services with `Authorization: Bearer ${process.env.AUTH_TOKEN}`, the service validates it:\n\n```http\nPOST /v1/authorities/ath_acme/validate HTTP/1.1\nHost: auth.fullstacked.cloud\nX-FullStacked-Authority-Key: sec_auth_...\nContent-Type: application/json\n\n{ \"token\": \"fsc_ath_acme_...\" }\n```\n\n`200`:\n\n```json\n{\n  \"valid\": true,\n  \"authorityId\": \"ath_acme\",\n  \"userId\": \"alice\",\n  \"email\": \"alice@example.com\",\n  \"name\": \"Alice Martin\",\n  \"groups\": [\"devops\"],\n  \"tokenClass\": \"ath\",\n  \"tokenId\": \"tok_...\",\n  \"issuedAt\": 1790000000,\n  \"expiresAt\": 1790086400,\n  \"metadata\": {}\n}\n```\n\n| Response | Meaning |\n| :--- | :--- |\n| `401 { \"valid\": false, \"error\": \"token_invalid_or_expired\" }` | Not an `fsc_ath_` token, or unknown, expired or revoked. |\n| `400` | Token issued by another authority, or `token` missing. |\n| `401` | Wrong authority key. |\n| `403` | Authority suspended. |\n| `429 RATE_LIMITED` | Over 100 validations per second per authority. |\n\nOnly `fsc_ath_` tokens of your own authority validate here. To check a platform user's token (`fsc_ses_`, `fsc_pat_`) instead, call `GET https://auth.fullstacked.cloud/api/v1/me` with it: `200` means valid, and the body says who it is.\n\n```ts\n// Express-style middleware in your own backend (Node.js, not the FullStacked runtime)\nexport async function fullstackedUser(req, res, next) {\n    const token = req.headers.authorization?.replace(/^Bearer\\s+/i, \"\");\n    const r = await fetch(`${process.env.FSC_AUTH_URL}/v1/authorities/${process.env.FSC_AUTHORITY_ID}/validate`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\", \"X-FullStacked-Authority-Key\": process.env.FSC_AUTHORITY_KEY! },\n        body: JSON.stringify({ token }),\n    });\n    const body = await r.json();\n    if (!r.ok || !body.valid) return res.status(401).json({ error: \"unauthorized\" });\n    req.user = body;\n    next();\n}\n```\n\nKeep the authority key on your server only: never ship it in a project.\n\n## Endpoints\n\n### POST /v1/auth/exchange/token\n\nRedeems a single-use fsc_exc_ exchange token with its PKCE code_verifier: a BYOA sign-in returns an fsc_ath_<slug>_ session token, a session handoff a fresh fsc_ses_ token.\n\n- **Service**: `auth` (`https://auth.fullstacked.cloud`)\n- **Auth**: Public (PKCE code_verifier)\n- **In project code**: `${process.env.AUTH_URL}/v1/auth/exchange/token`\n\nRequest:\n\n```json\n{\n  \"exchangeToken\": \"fsc_exc_...\",\n  \"codeVerifier\": \"...\"\n}\n```\n\n### POST /v1/authorities/:id/token\n\nServer-to-server: an enterprise IdP mints a PKCE-bound fsc_exc_ exchange token for { userId, email?, name?, groups?, ttl?, codeChallenge, metadata? }.\n\n- **Service**: `auth` (`https://auth.fullstacked.cloud`)\n- **Auth**: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator\n\n### POST /v1/authorities/:id/validate\n\nValidates an fsc_ath_<slug>_ token issued for this authority and returns its claims (userId, email, name, groups, tokenId, issuedAt, expiresAt, metadata).\n\n- **Service**: `auth` (`https://auth.fullstacked.cloud`)\n- **Auth**: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator\n\nRequest:\n\n```json\n{\n  \"token\": \"fsc_ath_acme_...\"\n}\n```\n\ncurl:\n\n```bash\ncurl -sS -X POST -H \"X-FullStacked-Authority-Key: $AUTHORITY_KEY\" -H \"Content-Type: application/json\" -d '{\"token\":\"fsc_ath_...\"}' \"https://auth.fullstacked.cloud/v1/authorities/<authorityId>/validate\"\n```\n\n### POST /v1/authorities/:id/revoke\n\nRevokes the authority's tokens by { userId }, by { token }, or { all: true }.\n\n- **Service**: `auth` (`https://auth.fullstacked.cloud`)\n- **Auth**: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator\n"}],"endpoints":[{"service":"auth","topic":"auth-byoa","id":"auth.exchangeToken","method":"POST","path":"/v1/auth/exchange/token","auth":"Public (PKCE code_verifier)","summary":"Redeems a single-use fsc_exc_ exchange token with its PKCE code_verifier: a BYOA sign-in returns an fsc_ath_<slug>_ session token, a session handoff a fresh fsc_ses_ token.","inProjectUrlExpression":"${process.env.AUTH_URL}/v1/auth/exchange/token","requestExample":{"exchangeToken":"fsc_exc_...","codeVerifier":"..."},"fullUrl":"https://auth.fullstacked.cloud/v1/auth/exchange/token"},{"service":"auth","topic":"auth-byoa","id":"auth.authorityToken","method":"POST","path":"/v1/authorities/:id/token","auth":"X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator","summary":"Server-to-server: an enterprise IdP mints a PKCE-bound fsc_exc_ exchange token for { userId, email?, name?, groups?, ttl?, codeChallenge, metadata? }.","fullUrl":"https://auth.fullstacked.cloud/v1/authorities/:id/token"},{"service":"auth","topic":"auth-byoa","id":"auth.authorityValidate","method":"POST","path":"/v1/authorities/:id/validate","auth":"X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator","summary":"Validates an fsc_ath_<slug>_ token issued for this authority and returns its claims (userId, email, name, groups, tokenId, issuedAt, expiresAt, metadata).","requestExample":{"token":"fsc_ath_acme_..."},"codeExamples":{"curl":"curl -sS -X POST -H \"X-FullStacked-Authority-Key: $AUTHORITY_KEY\" -H \"Content-Type: application/json\" -d '{\"token\":\"fsc_ath_...\"}' \"https://auth.fullstacked.cloud/v1/authorities/<authorityId>/validate\""},"fullUrl":"https://auth.fullstacked.cloud/v1/authorities/:id/validate"},{"service":"auth","topic":"auth-byoa","id":"auth.authorityRevoke","method":"POST","path":"/v1/authorities/:id/revoke","auth":"X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator","summary":"Revokes the authority's tokens by { userId }, by { token }, or { all: true }.","fullUrl":"https://auth.fullstacked.cloud/v1/authorities/:id/revoke"}]}