Skip to main content
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).

Google client id and secret, locally

1

Create the OAuth client

In Google Cloud console → APIs & Services → 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

dashboard/api/.env
Or export the same variables in the shell that starts the API.
4

Start and try it

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

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

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.