Runtime Sign-In & BYOA
Projects 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).
Interactive sign-in from a runtime app
fullstacked/auth exports one function:
authenticate(url: string, opts?: { windowFeatures?: string }): Promise<any>It 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.
Flow:
- Create a PKCE pair:
code_verifier(32 random bytes, base64url) andcode_challenge= base64url(SHA-256(verifier)). authenticate(`${AUTH_URL}/?code_challenge=<challenge>&code_challenge_method=S256`). The user signs in and approves on the consent screen.- Redeem:
POST ${AUTH_URL}/v1/auth/exchange/tokenwith{ exchangeToken, codeVerifier }. The response carries the session token.
import { authenticate } from "fullstacked/auth";
import { createHash, randomBytes } from "node:crypto";
export async function signIn(authUrl: string): Promise<string> {
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const result = await authenticate(
`${authUrl}/?code_challenge=${challenge}&code_challenge_method=S256`
);
if (result?.error) throw new Error(result.error);
if (!result?.exchange_token?.startsWith("fsc_exc_")) throw new Error("Invalid authentication response");
const res = await fetch(`${authUrl}/v1/auth/exchange/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ exchangeToken: result.exchange_token, codeVerifier: verifier }),
});
if (!res.ok) throw new Error(`Exchange failed (${res.status}): ${await res.text()}`);
const session = await res.json();
return session.token; // fsc_ses_... (or fsc_ath_<slug>_... from an enterprise authority)
}Call signIn from a click handler so the window can open. Use node crypto (core-backed), not crypto.subtle.
Exchange response (200):
{
"token": "fsc_ses_...",
"tokenType": "Bearer",
"userId": "usr_4f1c9a2b7d3e8f6a5b0c1d2e",
"sessionId": "tok_...",
"user": { "userId": "usr_4f1c9a2b7d3e8f6a5b0c1d2e", "email": "[email protected]", "name": "Alice Martin", "slug": "alice", "createdAt": "2026-09-20T14:32:00.000Z" },
"session": { "sessionId": "tok_...", "userId": "usr_4f1c9a2b7d3e8f6a5b0c1d2e", "expiresAt": 1792000000000 },
"issuedAt": 1790000000,
"expiresAt": 1792000000,
"restricted": false
}Rules:
- The challenge is mandatory and must be S256 (43 base64url characters); without it the auth page refuses to start.
- 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). bodykeys also acceptexchange_token/code_verifier.- Sign out with
POST ${AUTH_URL}/v1/auth/logoutandAuthorization: Bearer <token>(returns{ "status": "revoked" }). - Store the token like a password (for example
fullstacked/configis plain text in the project directory: acceptable on the user's own device only).
Enterprise Bring Your Own Auth (BYOA)
An 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.
Minting a sign-in for your user (your IdP backend)
After 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.
- Native (
native=1): redirect to<callback_scheme>://auth?exchange_token=fsc_exc_...(on denial?error=<message>), that isfullstacked-auth://auth?.... - Popup (no
native):window.opener.postMessage({ exchange_token }, <opener origin>)(on denial{ error }), then close the window.
The runtime app then redeems it at https://auth.fullstacked.cloud/v1/auth/exchange/token exactly as above and receives fsc_ath_<slug>_....
POST /v1/authorities/ath_acme/token HTTP/1.1
Host: auth.fullstacked.cloud
X-FullStacked-Authority-Key: sec_auth_...
Content-Type: application/json
{ "userId": "alice", "codeChallenge": "<S256 challenge from the runtime>", "email": "[email protected]", "name": "Alice Martin", "groups": ["devops"] }Response: { "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.
Validating a user's token in your backend
When a project calls one of your services with Authorization: Bearer ${process.env.AUTH_TOKEN}, the service validates it:
POST /v1/authorities/ath_acme/validate HTTP/1.1
Host: auth.fullstacked.cloud
X-FullStacked-Authority-Key: sec_auth_...
Content-Type: application/json
{ "token": "fsc_ath_acme_..." }200:
{
"valid": true,
"authorityId": "ath_acme",
"userId": "alice",
"email": "[email protected]",
"name": "Alice Martin",
"groups": ["devops"],
"tokenClass": "ath",
"tokenId": "tok_...",
"issuedAt": 1790000000,
"expiresAt": 1790086400,
"metadata": {}
}| Response | Meaning |
|---|---|
401 { "valid": false, "error": "token_invalid_or_expired" } | Not an fsc_ath_ token, or unknown, expired or revoked. |
400 | Token issued by another authority, or token missing. |
401 | Wrong authority key. |
403 | Authority suspended. |
429 RATE_LIMITED | Over 100 validations per second per authority. |
Only 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.
// Express-style middleware in your own backend (Node.js, not the FullStacked runtime)
export async function fullstackedUser(req, res, next) {
const token = req.headers.authorization?.replace(/^Bearer\s+/i, "");
const r = await fetch(`${process.env.FSC_AUTH_URL}/v1/authorities/${process.env.FSC_AUTHORITY_ID}/validate`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-FullStacked-Authority-Key": process.env.FSC_AUTHORITY_KEY! },
body: JSON.stringify({ token }),
});
const body = await r.json();
if (!r.ok || !body.valid) return res.status(401).json({ error: "unauthorized" });
req.user = body;
next();
}Keep the authority key on your server only: never ship it in a project.
Endpoints
POST /v1/auth/exchange/token
Redeems a single-use fsc_exc_ exchange token with its PKCE code_verifier: a BYOA sign-in returns an fsc_ath_
- Service:
auth(https://auth.fullstacked.cloud) - Auth: Public (PKCE code_verifier)
- In project code:
${process.env.AUTH_URL}/v1/auth/exchange/token
Request:
{
"exchangeToken": "fsc_exc_...",
"codeVerifier": "..."
}POST /v1/authorities/:id/token
Server-to-server: an enterprise IdP mints a PKCE-bound fsc_exc_ exchange token for { userId, email?, name?, groups?, ttl?, codeChallenge, metadata? }.
- Service:
auth(https://auth.fullstacked.cloud) - Auth: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator
POST /v1/authorities/:id/validate
Validates an fsc_ath_
- Service:
auth(https://auth.fullstacked.cloud) - Auth: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator
Request:
{
"token": "fsc_ath_acme_..."
}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"POST /v1/authorities/:id/revoke
Revokes the authority's tokens by { userId }, by { token }, or { all: true }.
- Service:
auth(https://auth.fullstacked.cloud) - Auth: X-FullStacked-Authority-Key, or Bearer fsc_* of an authority administrator