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

# Sign-in

> Google sign-in for the dashboard and the CLI, how to configure it, and running without it

The dashboard API owns sign-in. With it on, people sign in with Google, each email domain (or each person) gets
a workspace, and `levers9 login` fetches a token through the same flow. With it off, the dashboard acts as the
one workspace whose token it was started with and the CLI uses `levers9 config create`.

## Settings

All settings are environment variables with the `LEVERS9_` prefix, read by the dashboard API. Locally they can
also live in a `.env` file next to `dashboard/api/pyproject.toml` (it is gitignored).

| variable | what |
| - | - |
| `LEVERS9_TOKEN` | the cluster-admin token the dashboard acts with: provisioning workspaces at sign-in, listing workers and pools, the log spooler. On EKS it comes from the `levers9-dashboard-env` secret; locally put the same token in `.env`. The CLI's default context is only a fallback when this is unset, and a plain workspace token there leaves Workers and Pools empty |
| `LEVERS9_AUTH_ENABLED` | `true` turns sign-in on |
| `LEVERS9_AUTH_SECRET` | signs the session cookie; any long random string, keep it stable across restarts |
| `LEVERS9_GOOGLE_CLIENT_ID` / `LEVERS9_GOOGLE_CLIENT_SECRET` | the OAuth client from Google Cloud (below) |
| `LEVERS9_APP_URL` | where the web app is, `http://localhost:3000` locally |
| `LEVERS9_API_URL` | this API as the browser reaches it, `http://localhost:8100` locally; the OAuth redirect URI is built from it |
| `LEVERS9_AUTH_ALLOWED_DOMAINS` | JSON list, `["uselevers.com"]`, of domains that may sign in at all; empty allows any Google account. Anyone else lands on the **waitlist**, and a platform admin (`LEVERS9_ADMIN_EMAILS`) sees them under **Members → Waiting to join** with **Let in** and **Deny**. Letting someone in adds them to that admin's workspace; invited emails are always allowed. This decides who may reach the door. A workspace's own join policy decides who comes through it, and that workspace's admins see the requests waiting on it, see [Joining a workspace](https://levers9.mintlify.site/guides/identity.md#joining-a-workspace) |
| `LEVERS9_AUTH_WORKSPACE_MODE` | `domain` (one workspace per email domain, the default), `user` (one per person, nothing shared), or `both` (an organisation workspace *and* a personal one each; you land in the organisation's) |
| `LEVERS9_AUTH_COOKIE_DOMAIN` | `.uselevers.com` in production. The signed-in marker lives there so the app's host can see it, and so does the session unless `LEVERS9_AUTH_SESSION_HOST_ONLY` is on. Leave it empty locally |
| `LEVERS9_AUTH_SESSION_HOST_ONLY` | `true` keeps the session cookie on the API host alone, so tenant endpoints and sandboxes on sibling hosts never get it. Default `false`; see [Sessions and cookies](#sessions-and-cookies) for when to turn it on |
| `LEVERS9_AUTH_ORIGIN_CHECK` | `observe` (default), `enforce` or `off`. Whether a change made with the session cookie has to come from the web app or the API's own pages, see [Sessions and cookies](#sessions-and-cookies) |
| `LEVERS9_AUTH_SESSION_DAYS` | how long a session lasts, default 30 |
| `LEVERS9_AUTH_IMPERSONATION_MINUTES` | how long a platform admin's **View as** lasts before the session goes back to their own workspace, default 120 |
| `LEVERS9_AUTH_SESSION_ID_REQUIRED` | `true` refuses session cookies from before sessions had ids. Default `false`; turn it on once `LEVERS9_AUTH_SESSION_DAYS` have passed since the release that added them |
| `LEVERS9_AUTH_CLI_TOKEN_REDIRECT` | `true` (default) still hands an older `levers9 login`, one that sends no code challenge, its token in the loopback redirect. `false` tells such a CLI to upgrade instead |
| `LEVERS9_AUTH_TOKEN_IN_GET` | `true` (default) keeps the token in `GET /api/agent/connection` and `GET /auth/cli/token` for web pages from before `POST /api/agent/token`. `false` hands it out only on that POST |
| `LEVERS9_AUTH_DEV_LOGIN` | `true` enables `/auth/dev-login?email=…`, a sign-in box that accepts any email, for local development only |
| `LEVERS9_CLI_CONTROLPLANE_HOST` / `LEVERS9_CLI_CONTROLPLANE_PORT` | what `levers9 login` writes into the CLI config when the controlplane is reached at a different address than the API uses |

## Google client id and secret, locally

**1. Create the OAuth client**

In [Google Cloud console → APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials),
**Create credentials → OAuth client ID**, application type **Web application**. If the project has no consent
screen yet, configure one first (External, your app name, your email; add yourself as a test user while it is
in testing).

**2. Add the local origins**

Authorised JavaScript origin: `http://localhost:3000`.
Authorised redirect URI: `http://localhost:8100/auth/callback` (that is `LEVERS9_API_URL` + `/auth/callback`).
For production add `https://levers9-api.uselevers.com/auth/callback` and the app origin.

**3. Put the values in .env**

```bash dashboard/api/.env theme={null}
LEVERS9_AUTH_ENABLED=true
LEVERS9_AUTH_SECRET=change-me-to-something-long
LEVERS9_GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
LEVERS9_GOOGLE_CLIENT_SECRET=GOCSPX-...
LEVERS9_APP_URL=http://localhost:3000
LEVERS9_API_URL=http://localhost:8100
```

Or export the same variables in the shell that starts the API.

**4. Start and try it**

```bash theme={null}
cd dashboard/api && uv run uvicorn app.main:app --port 8100
cd dashboard/web && npm run dev
```

Open `http://localhost:3000/apps`; you are sent to the sign-in page and back. The API has no auto-reload, so
restart it after changing `.env`.

> [!NOTE]
>
> No Google project at hand? Set `LEVERS9_AUTH_DEV_LOGIN=true` and leave the client id empty: the sign-in page
> becomes an email box, and workspaces, environments and members work the same. Never enable that in production.

Once someone is signed in, what they and their tools carry afterwards — sessions, tokens, platform admins and
how access is removed — is [Identity and credentials](https://levers9.mintlify.site/guides/identity.md).

## What sign-in creates

The first sign-in from a domain provisions a controlplane workspace with the cluster-admin token the API holds, and
stores it in the API's SQLite database (`~/.levers9/dashboard/auth.db`). Later sign-ins from the same domain join
it. New [environments and team workspaces](https://levers9.mintlify.site/dashboard/workspaces.md) provision more controlplane workspaces the same way.

## The CLI

`levers9 login [--env NAME] [--name CONTEXT] [--url https://levers9-api.uselevers.com]` starts a loopback server,
opens the browser on the API's sign-in, and gets the environment's token; nothing is typed or shown.
`--no-browser` prints the URL instead. On a cluster without sign-in the command says so and points to
`levers9 config create`.

With a current CLI the token doesn't travel in a URL. The CLI sends a PKCE challenge (`code_challenge`, S256) with
the sign-in, the browser comes back to the loopback port with a one-time `code`, and the CLI trades that code and
its secret verifier for the token with `POST /auth/cli/exchange`. A code is good for about a minute and once, and
it's useless without the verifier, which never leaves the CLI. A CLI from before this sends no challenge and still
gets its token in the redirect, as it always did, while `LEVERS9_AUTH_CLI_TOKEN_REDIRECT` is on. Once nobody's on
an older CLI (`/api/sdk` lists the versions in use), turn it off and an older CLI is told to upgrade. A newer CLI
still takes a token from a dashboard that ignored its challenge.

It can also come back with nothing to save. Someone whose request to join a workspace is waiting has signed in
but has no token to collect, so the dashboard answers the loopback port with that status rather than a token, and
the command says so and exits 1 without writing a context:

```
✗ uselevers admits people by invitation, so your request to join has been passed to its admins. Sign in
  again once an admin has let you in.
  hint: An admin lets people in under Members in the dashboard.
```

## Sessions and cookies

A sign-in writes two cookies. `levers9_session` is the session: signed, `HttpOnly`, and it names a row in the
sessions table, so `POST /auth/logout` ends that session for good (a copy of the cookie stops working too)
and `POST /auth/logout-everywhere` ends every session the person has, in every browser. Cookies from before
sessions had ids keep working until they expire, so nobody's signed out by an upgrade; sign out everywhere ends
those as well. `levers9_signed_in=1` is a marker with nothing secret in it. It sits on the cookie domain so the
web app's host can tell someone's signed in, and every API call still checks the real session.

Tenant endpoints, MCP servers and sandbox ports are served from a sibling host (`levers9.uselevers.com`), so a
session cookie on `.uselevers.com` reaches them too. Two settings close that:

* `LEVERS9_AUTH_SESSION_HOST_ONLY=true` keeps the session on the API host. A browser's existing session is
  moved over on its next request, keeping its expiry, and the domain-wide copy is deleted, so nobody has to
  sign in again. Turn it on only once the web app looks for the marker rather than the session, or every app
  page loops back to the sign-in page.
* `LEVERS9_AUTH_ORIGIN_CHECK` looks at every change made with the session cookie (POST, PUT, PATCH, DELETE
  and websockets). It has to come from the web app (`LEVERS9_CORS_ORIGINS`, `LEVERS9_APP_URL`) or the API's own
  pages (JupyterLab in its own tab, Swagger at `/docs`). A request with no `Origin`, which is what the CLI,
  scripts and call platforms send, passes unless the browser says it was cross-site. Requests with a bearer
  token aren't checked. `observe` logs what it would refuse (method, path, origin and user id), `enforce` answers
  `403`. Watch the log for a while in `observe` before you switch.

A platform admin's **View as** lasts `LEVERS9_AUTH_IMPERSONATION_MINUTES` (two hours by default). After that
the session's back in the admin's own workspace, the same as pressing the button.

The sign-in pages the API serves itself (a cancelled sign-in, the waiting page, the development sign-in) escape
everything they echo and carry a Content-Security-Policy that allows no scripts.

## In production

`deploy/eks/dashboard.yaml` sets the same variables from a Kubernetes secret (`levers9-dashboard-env`), with the
cookie domain `.uselevers.com` and the redirect URI on the API hostname. The secret keys survive redeploys. The
order for turning on the stricter cookie settings is in `deploy/eks/README.md`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.