> ## Documentation Index
> Fetch the complete documentation index at: https://getfloo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Add User Auth to Your App

> Set access_mode = "accounts" and your app gets a hosted sign-in flow, identity headers on every request, and a per-app user list — without writing any auth code.

floo manages user authentication for your deployed apps. When you set `access_mode = "accounts"`, the floo gateway puts a hosted sign-in flow in front of your app, validates each user's session, and injects identity headers into every request before it reaches your code.

You write no auth code. No login pages. No session storage. No OAuth flow. Your app reads `X-Floo-User-Email` from the request headers and renders a personalized response. That's the whole integration.

## Quickstart

Three steps from a deployed app to per-user sign-in.

### 1. Set the access mode

In `floo.app.toml`:

```toml theme={null}
[app]
name = "my-app"
access_mode = "accounts"

[services.web]
type = "web"
port = 3000
ingress = "public"
```

That's the entire auth config. There is no `[auth]` section to add, no callback URLs to register, no client ID to provision.

### 2. Deploy

```bash theme={null}
git push origin main
```

When the deploy completes, every request to `https://my-app-dev.on.getfloo.com` (and the prod URL after promotion) is intercepted by the floo gateway. Unauthenticated visitors are redirected to a hosted sign-in page powered by floo's WorkOS integration — they can sign in with email magic link, Google, GitHub, or any provider you've enabled in your org settings.

### 3. Read the user from request headers

Every request that reaches your app's container has the signed-in user attached:

```
X-Floo-User-Email: jane@acme.com
X-Floo-User-Id:    01HQK4...
X-Floo-User-Name:  Jane Doe
X-Floo-User-Role:  member
```

Read them like any other header. Examples for each major stack are in the [Build with...](/build/rails) guides.

<Check>
  **Your app has per-user auth.** The gateway redirects unauthenticated requests, manages the session cookie, and tells you who the user is on every request. Your app code did not change to make this work.
</Check>

## What gateway-managed auth gives you

|                         |                                                                                                                                                                                                                                  |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Hosted sign-in page** | Branded login flow served by floo (email magic link, Google, GitHub, and any provider you've enabled in your org settings). The gateway redirects unauthenticated visitors automatically — your app never sees the sign-in flow. |
| **Session management**  | A signed `__floo_session` cookie, validated on every request, rolled forward as users stay active, revoked on sign-out.                                                                                                          |
| **Identity headers**    | `X-Floo-User-Email`, `X-Floo-User-Id`, `X-Floo-User-Name`, `X-Floo-User-Role` injected into every authenticated request.                                                                                                         |
| **Logout endpoint**     | `POST` (or `DELETE`) `/__floo/logout` clears the session and redirects to the floo login page. After re-login the user lands at the app root `/`. GET is rejected with 405 — see the sign-out section below for why.             |
| **User profile JSON**   | `GET /__floo/me` returns `{ user_id, email, name, role }`. Useful for client-side code that needs the user without parsing headers.                                                                                              |
| **Per-app user list**   | Every signed-in user appears in your app's Users tab in the dashboard. First-seen, last-active, sign-in count — for free.                                                                                                        |
| **Access policies**     | Restrict sign-in to your team (`@acme.com` only), specific invited users, or open enrollment.                                                                                                                                    |

## Restrict who can sign in

By default, anyone with a valid email can sign in. Restrict access in three ways.

### Company domain allowlist

In the dashboard, under **App → Users → Access**, add your company email domain (e.g. `acme.com`). Only users whose verified email matches will be allowed in. Consumer mailbox providers (gmail.com, outlook.com, etc.) are rejected by default.

### Invite-only

Set the access policy to invite-only and add specific emails. Users not on the list see an "access denied" page after authenticating.

### Declarative access policy

Configure access policy and allowlisted company domains directly in `floo.app.toml`:

```toml theme={null}
[auth]
access_policy = "domain"          # "open", "invite", or "domain"
allowed_domains = ["acme.com"]    # required when access_policy = "domain"
```

Both fields sync to the App on every deploy. The dashboard's per-app **Access** tab is the alternative — same fields, edited through the UI. Domain allowlists are a Pro+ feature; consumer mailbox providers (gmail.com, outlook.com, etc.) are rejected by default.

## Access modes

| Mode       | Description                               | Plan |
| ---------- | ----------------------------------------- | ---- |
| `public`   | No auth, anyone can access                | All  |
| `password` | Shared app password for simple protection | Pro+ |
| `accounts` | Per-user auth via gateway-managed sign-in | Pro+ |

Enterprise SSO (SAML/OIDC) is handled as a sales-assisted setup rather than a self-serve `access_mode` — [contact sales](mailto:sales@getfloo.com) if your team needs it.

### Per-environment overrides

```toml theme={null}
[app]
name = "my-app"
access_mode = "accounts"

[environments.dev]
access_mode = "public"
```

The override wins for that environment. Common pattern: `public` in dev so iteration is fast, `accounts` in prod.

### Password-protected apps

For a single shared password (no user accounts), set the access mode in `floo.app.toml`:

```toml theme={null}
[app]
access_mode = "password"
```

The platform generates the shared password automatically on the next deploy. Retrieve it with:

```bash theme={null}
floo apps password my-app
```

Anyone with the password gets in; there is no per-user identity, and identity headers are not injected.

## Reading the user in your app

The pattern is the same in every stack: read the request header.

```js theme={null}
// Express
app.use((req, _res, next) => {
  const email = req.get("x-floo-user-email");
  req.flooUser = email ? {
    email,
    id: req.get("x-floo-user-id"),
    name: req.get("x-floo-user-name"),
  } : null;
  next();
});
```

```python theme={null}
# FastAPI
def require_user(
    email: Annotated[str | None, Header(alias="X-Floo-User-Email")] = None,
):
    if not email: raise HTTPException(401)
    return email
```

```ruby theme={null}
# Rails
class ApplicationController < ActionController::Base
  before_action :load_floo_user
  private
  def load_floo_user
    @current_user_email = request.headers["X-Floo-User-Email"]
  end
end
```

For full stack-specific examples, see the [Build with…](/build/rails) guides.

### `/__floo/me` — fetch the user as JSON

For client-side code that needs the user object, hit `/__floo/me` from the browser:

```js theme={null}
const res = await fetch("/__floo/me");
const user = await res.json();
// { user_id, email, name, role }
```

Returns `401` if the session is missing or expired. The endpoint is on your app's own host, so no CORS to configure.

### `/__floo/logout` — sign out

`/__floo/logout` accepts `POST` and `DELETE`. Other methods (including GET) return `405 Method Not Allowed` with `Allow: POST, DELETE`. On a successful POST/DELETE the gateway clears the `__floo_session` cookie, invalidates the session in floo's session store, and returns `302` to the floo managed-auth login page. After the user signs back in they land at the app's root (`/`) — not the page they were on when they signed out.

**Why no plain `<a href="/__floo/logout">` link?** The session cookie is `SameSite=Lax`, which still sends the cookie on cross-origin top-level GET navigation (a malicious link the user clicks, or a `window.location` redirect from another tab). A GET-served logout was reachable from any other site and could force a sign-out. POST and DELETE are not sent cross-origin under Lax under any condition, so restricting to those methods closes the vector.

**Sign-out clears floo's session, not the upstream identity provider.** If the user originally signed in via WorkOS and their WorkOS session is still active, the next visit will silently re-authenticate without a password prompt — exactly like browser SSO across other apps. To force a full re-login, the user has to sign out of the IdP separately. (Single-Logout / federated sign-out is not yet supported.)

**Form button (POST):**

```html theme={null}
<form method="POST" action="/__floo/logout">
  <button type="submit">Sign out</button>
</form>
```

**Rails / Turbo:**

```erb theme={null}
<%= button_to "Sign out", "/__floo/logout", method: :delete %>
```

or with Turbo's data attribute on a link:

```erb theme={null}
<%= link_to "Sign out", "/__floo/logout", data: { turbo_method: :delete } %>
```

**JavaScript fetch:**

```js theme={null}
await fetch("/__floo/logout", { method: "POST", redirect: "manual" });
window.location.href = "/";  // browser won't follow 302 from fetch with manual redirect
```

## Local development

The floo gateway isn't in the request path locally, so by default no identity headers reach your app. You have two options.

### Use `floo dev --fixture-user`

For accounts-mode apps, `floo dev --fixture-user EMAIL` starts a small in-process proxy in front of each service that injects the same `X-Floo-User-*` headers floo's gateway adds in production:

```bash theme={null}
floo dev --app my-app --fixture-user you@example.com
```

Output shows two URLs per service — the raw service URL and the auth-proxied URL:

```
Service   Port   URL                       Auth-proxied URL
─────────────────────────────────────────────────────────────────
web       3000   http://localhost:3000     http://localhost:14732
```

Hit the **auth-proxied URL** when you want to test signed-in flows — your app sees `X-Floo-User-Email`, `X-Floo-User-Id`, `X-Floo-User-Name`, and `X-Floo-User-Role` exactly as it would in production. Hit the raw URL for unauthenticated paths or quick checks.

Optional flags fill in the rest of the fixture user (defaults shown):

| Flag                  | Default                         |
| --------------------- | ------------------------------- |
| `--fixture-id ID`     | `dev-fixture-<email-localpart>` |
| `--fixture-name NAME` | the email                       |
| `--fixture-role ROLE` | `member`                        |

The proxy only runs for apps with `access_mode = "accounts"` — if you pass `--fixture-user` against any other access mode, `floo dev` warns and skips the proxy.

### Inject the headers yourself

For one-off `curl` testing, scripts, or stacks where you'd rather skip the proxy entirely:

```bash theme={null}
curl -H "X-Floo-User-Email: you@example.com" \
     -H "X-Floo-User-Id: dev-user-1" \
     -H "X-Floo-User-Name: You" \
     http://localhost:3000/dashboard
```

Or wrap your header-reading helper to fall back to a fixture user when `process.env.NODE_ENV === "development"` (or your stack's equivalent).

## Troubleshooting

### Users see a login page on every request

The session cookie isn't sticking. Most likely your app sits behind a TLS-terminating proxy that strips `Set-Cookie` headers, or you've misconfigured a custom domain. Check the app's response headers — a successful login sets `__floo_session` on the app's host.

### Identity headers are missing from requests

The app probably wasn't redeployed after setting `access_mode = "accounts"`. Push (or `floo redeploy --app my-app`) to pick up the gateway routing change.

### Users from outside my company can sign in

You haven't configured an access policy. Set a domain allowlist in the dashboard or in `floo.app.toml` under `[auth].access_policy`.

### I want users to land on a specific page after login

The gateway redirects to the original requested URL after sign-in — so just link unauthenticated users to the deep link they wanted, and the gateway does the right thing. To force a landing page, redirect from `/` based on `X-Floo-User-Email`.

<CardGroup cols={2}>
  <Card title="Build a stack-specific app" href="/build/rails">
    Stack journeys (Rails, Next.js, FastAPI, Django, Express) showing the full deploy + auth flow.
  </Card>

  <Card title="Team Access" href="/guides/team-access">
    Org membership, app access policies, and password-protected apps.
  </Card>

  <Card title="Internal Tools" href="/companies/internal-tools">
    Why managed auth is the right answer for internal tools — and how a domain allowlist gives you a per-team app in one config change.
  </Card>

  <Card title="Custom Domains" href="/guides/custom-domains">
    Put your auth-protected app on `app.yourcompany.com`.
  </Card>
</CardGroup>
