Forge Identity is the optional app-scoped account and team service for applications hosted by Forge Deploy. It lets an application recognize a visitor without receiving that visitor's Forge control-plane session.
import { defineForge } from "@smolai/forge/config";
export default defineForge({
version: 1,
name: "team-dashboard",
app: {
assets: { directory: "dist", fallback: "index.html" },
identity: {
scopes: ["user:basic", "accounts:read", "teams:read"],
},
},
routes: [{ pattern: "/*", to: "app.assets" }],
});user:basic discloses an app-specific random user ID and the visitor's public
Forge profile. accounts:read adds the visitor's current accounts and roles.
teams:read adds only teams the visitor currently belongs to.
The versioned browser client is hosted by Forge:
import { forge } from "/.forge/client.js";
const user = await forge.identity.currentUser();
if (!user) {
forge.identity.login({ returnTo: location.pathname });
}
const accounts = await forge.identity.listAccounts();
const teams = await forge.identity.listTeams();
await forge.identity.logout();For a complete sign-in and account control without adopting a UI framework, mount Forge's web component:
<script type="module">
import { forge } from "/.forge/client.js";
forge.identity.mountAccountControl("#account", {
returnTo: location.pathname,
});
document.querySelector("#account").addEventListener(
"forge-account-select",
({ detail }) => {
// This is app-local context. It does not change Forge ownership or plans.
console.log("Selected account", detail.account);
},
);
</script>
<div id="account"></div>The same control is available declaratively as
<forge-account-control return-to="/dashboard">. It renders in Shadow DOM,
works without React, lists only the accounts and teams disclosed to this app,
and emits forge-identity-change when its session changes. Account selection
is deliberately an app event rather than an implicit platform mutation.
Dynamic Forge applications receive the same app-scoped principal through the trusted wrapper:
export default {
async fetch(_request, env) {
const user = env.forge.identity.currentUser();
if (!user) return new Response("Sign in", { status: 401 });
const account = env.forge.identity.requireAccountRole(
"acct_org_example",
["owner", "admin"],
);
return Response.json({ user, account });
},
};The visitor is embedded in Forge's trusted request environment. Native hosted Workers receive it in the edge-signed, short-lived deployment context. Connected Workers on an active custom domain receive the same API from a Forge-published wrapper backed by a private, release-scoped service binding. Repository code never sees or introspects the raw session cookie.
forge.smol.ai; the control-plane bearer token is
used only on that origin.https://<site>/.forge/identity/callback URI with a 60-second one-time code.__Host-forge-session with Secure, HttpOnly, SameSite=Lax, and no
Domain.The platform layer removes both Forge Identity cookies before dispatching a
request to repository code. It also rejects any application response that
attempts to set a __Host-forge-* cookie. Connected code receives only the
frozen env.forge.identity facade; the private service binding and its
release-scoped props are removed from its environment.
For a connected Worker, every Identity call revalidates the exact HTTPS
hostname against an attached, observed-active custom-domain record with active
DNS and certificate evidence. The project, account authorization generation,
active production release and provider version, provider target and binding
generation, manifest digest, callback origin, and session grant must all agree.
Workers.dev previews, *.sites.smol.ai connected previews, path-only routes,
detached domains, stale releases, and cross-project or cross-host sessions fail
closed.
Sessions have a 12-hour hard lifetime and no refresh token in v1. Account suspension, tier/entitlement changes, organization membership changes, and team membership changes advance authorization generations. A session carrying stale generation evidence fails closed on its next use.
Visitors can inspect connected hosted apps under Forge Settings → Connected Forge apps. That surface shows the exact disclosed scope categories, active session count, and last use. Ending sessions revokes every live session for that visitor and app; it does not delete the stable app-specific identity, and the visitor may authorize the app again later.
Hosted applications never receive Forge's control-plane cookie or bearer token, the JWT signing secret, D1, Cloudflare credentials, or an unrestricted organization directory. Product-specific permissions remain product-owned.
forgeBuild.ts explicitly
requests it and whose owning account has identity.enabled.*.sites.smol.ai. Connected callback origins are derived only from exact,
verified, active custom-domain and production-release records. Arbitrary
redirect URLs are rejected.Forge Identity remains provider-neutral. Forge owns pairwise app IDs, scopes, consent, sessions, revocation, and the application-facing API. Cloudflare Access, an enterprise OIDC provider, or another upstream login system may authenticate a Forge user, but it is not exposed as the connected app's identity contract and cannot mint Forge app sessions by itself.