Last updated 2026-09-04Development preview

Browser access and authentication

Follow a browser request through service exposure, sign-in, backend verification, and application authorization.


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.

RequestWhat the backend should do
No credential for a protected operationReject before reading private data
An unverified user ID in a body or queryTreat it as untrusted input, not proof of identity
An invalid or expired credentialFail verification
A valid credential for a user who lacks accessDeny the business operation
A valid credential for an authorized userPerform 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:

yaml
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: users

This 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:

yaml
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 typescript

For a plain browser example, add these elements to the page:

html
<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:

typescript
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:

yaml
apiVersion: fluffy-chainsaw.dev/local/v1
auth:
  users:
    developer:
      email: developer@local.test
      emailVerified: true

Open 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 methodRequirement 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.