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 theLEVERS9_ 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
4
Start and try it
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.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=truekeeps 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_CHECKlooks 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 noOrigin, 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.observelogs what it would refuse (method, path, origin and user id),enforceanswers403. Watch the log for a while inobservebefore you switch.
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.
