A browser needs an address it can call. A backend needs trustworthy evidence of who is calling. The application then needs to decide whether that person may perform the requested operation. These are three separate decisions: exposure, authentication, and authorization.
Fluffy's browser entrypoint and generated SDK handle exposure and the configured sign-in flow. Its backend auth client verifies the credential. Your handler owns authorization for the application's data and actions.
#Understand the request boundary
A web entrypoint serves browser assets or targets a runtime. Its exposedServices mapping names backend services available to browser code. Adding authentication to the entrypoint gives its generated package an auth export as well as service clients.
A sign-in result identifies a user; it does not grant that user access to every exposed endpoint. Send the credential to a backend handler that verifies it and applies your policy before reading or changing private data.
| Request | What the backend should do |
|---|---|
| No credential for a protected operation | Reject before reading private data |
| An unverified user ID in a body or query | Treat it as untrusted input, not proof of identity |
| An invalid or expired credential | Fail verification |
| A valid credential for a user who lacks access | Deny the business operation |
| A valid credential for an authorized user | Perform the operation through the runtime's own resource grants |
The backend's database or bucket permission belongs to the runtime. It does not encode which user owns a particular row or file. How access works explains this distinction from the resource side.
#Initialization and interaction are separate steps
Call auth.ready() during page initialization. It loads the deployment document and initializes the selected provider. Enable sign-in only after it succeeds.
Call auth.startLogin() directly from a user interaction. Awaiting unrelated work first can lose the browser's user activation, and opening a login popup automatically during page load is not supported. Handle cancelled or redirected login flows instead of assuming every attempt returns an authenticated state.
#Configure browser authentication
#Declare authentication and browser exposure
For an application named example-app, add an auth attachment, a verifier grant to the API, and browser exposure:
auth:
users:
claims:
required: [email]
optional: [emailVerified]
runtimes:
api:
uses:
auth:
- name: users
actions: [verify]
webEntrypoints:
web:
artifact: web-assets
output: dist
siteIds:
prod: your-unique-hosting-site
exposedServices:
api:
runtime: api
auth: usersThis is an excerpt for an existing API and web-assets build artifact. Keep their other declarations, implement your backend routes, and use your actual hosting site ID. A static entrypoint needs its build output; an entrypoint targeting a runtime uses targetRuntime instead of a static artifact/output pair.
Workspace policy selects the hosting and authentication products and binds the application's auth name:
resourceTypes:
webEntrypoints:
static-site:
product: gcp.firebase-hosting
authFoundations:
google-users:
product: gcp.identity-platform.firebase-google
environments:
prod:
webEntrypoints:
defaults: {type: static-site, provider: application}
authFoundations:
customer-users: {type: google-users, provider: application}
authBindings:
example-app: {users: customer-users}The provider must already specify the intended project and region. The binding key example-app is the application name, not the entrypoint name.
#Sign in from the browser
Generate the runtime and browser clients:
fluffy-chainsaw generate --application example-app --all --language typescriptFor a plain browser example, add these elements to the page:
<button id="sign-in" disabled>Sign in</button>
<output id="sign-in-status" aria-live="polite">Preparing sign-in…</output>The following TypeScript assumes it lives at the application root. Adjust the generated import's relative path for your source file, or use your framework's equivalent event binding:
import { auth, services } from './.fluffy/generated/web-entrypoints/web/typescript/index.ts'
const button = document.querySelector<HTMLButtonElement>('#sign-in')!
const output = document.querySelector<HTMLOutputElement>('#sign-in-status')!
auth
.ready()
.then(() => {
button.disabled = false
output.textContent = 'Ready to sign in'
})
.catch(() => {
output.textContent = 'Sign-in is unavailable'
})
button.addEventListener('click', async () => {
button.disabled = true
try {
const state = await auth.startLogin({ returnTo: '/' })
if (state.state !== 'authenticated') {
output.textContent = state.state
return
}
const credential = await auth.credential()
const response = await services.api.call({
method: 'POST',
path: '/verify',
headers: { authorization: `Bearer ${credential.value}` },
})
if (!response.ok) throw new Error(`Verification failed: ${response.status}`)
output.textContent = 'The backend verified your sign-in'
} catch {
output.textContent = 'Sign-in or verification did not complete'
} finally {
button.disabled = false
}
})This requires the backend to implement POST /verify; generation does not create it. The SDK reads the page's same-origin deployment document, so serve the page through Fluffy rather than opening its HTML file directly. Keep credentials out of logs and persistent browser storage owned by your application.
#Verify credentials in the backend
Parse the incoming Bearer authorization header and pass the token string as credential. Use the sidecar setup for your selected language. These examples use example-app and module example.com/app; adjust the imports to your application. TypeScript uses address and transport; Python uses channel. Go passes its connection and a deadline-bearing context to the function:
import { Metadata } from '@grpc/grpc-js'
import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as authv1 from './.fluffy/generated/runtimes/api/typescript/fluffy/auth/v1/auth.js'
const client = new runtimev1.AuthUsersClient(address, transport)
const verified = await new Promise<authv1.VerifyResponse>((resolve, reject) => {
client.verify(
authv1.VerifyRequest.fromPartial({ credential }),
new Metadata(),
{ deadline: Date.now() + 5000 },
(error, value) => {
if (error) reject(error)
else resolve(value)
},
)
})
const subject = verified.subject
if (!subject) throw new Error('verified subject is missing')Use that verified subject to look up your application's permissions. Verify ownership or membership before performing a protected operation.
#Run locally and deploy
Local authentication uses local fixtures and its local provider flow. Fixtures in fluffy-chainsaw.local.yaml do not create production users:
apiVersion: fluffy-chainsaw.dev/local/v1
auth:
users:
developer:
email: developer@local.test
emailVerified: trueOpen the URL printed by fluffy-chainsaw local, sign in, and confirm the backend verification succeeds. Also exercise missing credentials, invalid credentials, and a valid user who lacks access to a protected operation.
For GCP, enable the Google provider and authorize the hosting domains described in Deployment actions. Re-run validation after completing those provider actions.
#Reference: supported auth configuration
One application can declare at most one auth attachment. The supported claim names are email and emailVerified; a claim cannot be duplicated across required and optional lists. Runtime auth access uses the verify action. Browser auth is generated only for an entrypoint with a declared authentication attachment.
| Browser method | Requirement and result |
|---|---|
ready() | Initialize the provider and obtain the current auth state |
startLogin({returnTo}) | Requires completed initialization and active user interaction; returns the login state |
credential() | Requires initialized, authenticated state; obtains the provider credential |
signOut() | Clears the auth session through the provider and returns anonymous state |
returnTo must be a valid root-relative path, not an external URL or protocol-relative //host address. Interactive operations cannot overlap. E_AUTH_NOT_READY means initialization has not completed; E_AUTH_USER_ACTIVATION_REQUIRED means login was not started in a valid user interaction.
A missing auth export is a declaration/generation issue. A deployment-document failure is a serving/origin issue. A provider-disabled or unauthorized-domain error needs the corresponding provider configuration fixed. A verified user receiving a business-access denial can be the correct outcome of your application's authorization policy.
Continue with Feature flags to vary behavior for users who are already authorized to use it.