Auth Server Architecture
How the OpenQuok CLI auth server works — request flow, endpoints, and Postgres state.
Connect your agent today
Draft from chat, review in your calendar, and publish only what you approve.
Overview
The CLI auth server (agent/server) is a Node.js service that implements OAuth2 device flow. It exists so the CLI can authenticate without embedding OAuth client secrets.
Environment variables and templates live under Configuration - Agent. Implementation reference: agent/server/app.ts.
Browser routes on the web app: web/src/routes/(public)/cli/device.
OpenQuok hosted (Production)
Note
OPENQUOK_AUTH_SERVER (CLI) targets cli-auth.openquok.com for API calls only. Users never need to open that host in a browser when BROWSER_ORIGIN is set to www.openquok.com.
Request flow (device login)
Components and responsibilities
CLI
- Calls /device/code on the auth server API origin.
- Opens verification_uri in a browser (production: www.openquok.com/cli/device/verify).
- Polls /device/token until the user finishes approval.
Auth server (agent/server)
- Holds the OAuth client secret and performs the server-to-server exchange with the OpenQuok API.
- Stores short-lived device flow state in Postgres (not in memory).
- Returns an API token to the CLI once approval is complete.
- Emits browser URLs from BROWSER_ORIGIN when configured.
Web app (web, when BROWSER_ORIGIN ≠ SERVER_URL)
- Serves /cli/device/verify and proxies to the auth server using CLI_AUTH_SERVER_URL.
- Proxies /cli/device/callback so OAuth redirects stay on the trusted web origin.
OpenQuok (web + API)
- Hosts the approval UI at OPENQUOK_FRONTEND_URL + OPENQUOK_AUTHORIZE_PATH.
- Exchanges the OAuth authorization code for an API access token via /api/v1/oauth/token.
OAuth client credentials vs user access tokens
Deployers configure one OAuth application on the auth server (OPENQUOK_OAUTH_CLIENT_ID and OPENQUOK_OAUTH_CLIENT_SECRET). That pair identifies the platform CLI app to the API. It is not the credential end users send on programmatic routes.
| Credential | Who holds it | Purpose |
|---|---|---|
| oqs_… client secret | Auth server only | Server-to-server POST /api/v1/oauth/token after the user approves in the browser |
| opo_… access token | Each CLI user (stored locally after login) | Authorization: Bearer on /api/v1/public/* |
Every successful device login produces a new opo_… token. Tokens are not shared across users or workspaces.
Tip
This is not the same as Developers → Access. Workspace owners can also rotate a programmatic token under Developers → Access in the dashboard. That token is issued against the workspace OAuth app created under Developers → Apps (one app per workspace). It does not use OPENQUOK_OAUTH_CLIENT_ID on the API server — that env var is for the auth server deployment only. See Public API authentication.
Workspace scoping and subscription plans
Plan limits are not inherited from whoever operates cli-auth.openquok.com. They attach to the workspace the user selects when authorizing the app — not to the auth server. The steps below bind that workspace from device verification through each later public API request.
Device verification and OAuth UI
User completes device verify → OAuth authorize UI.
Workspace selection
User picks a workspace and chooses Authorize.
Issue programmatic token
API upserts oauth_authorizations (user, workspace, app) and issues opo_….
Enforce plan limits on each request
Each /public/* request: resolve token → organization_id → subscription guard for that workspace.
The auth server stores organization_id on the completed device_requests row (returned from token exchange) so the CLI knows which workspace the token belongs to.
Is a secret safe?
The shared secret is safe, because, the client secret only proves that your auth server may exchange authorization codes for that registered OAuth app. A Solo-plan user still receives a token bound to their Solo workspace; Ultimate-only capabilities remain blocked by the API subscription guard for that organization_id.
Warning
The OAuth app referenced by OPENQUOK_OAUTH_CLIENT_ID must exist in the same OpenQuok project as the API. Its redirect URL must match /cli/device/callback on BROWSER_ORIGIN (or SERVER_URL when browser steps run on the auth server). Register it with agent/server/scripts/generate-oauth-app-env.mjs or manually under Developers → Apps in a workspace you control, then copy the client id and secret into the auth server env.
Workspace isolation
Users do not share one workspace.
Device login does not hand every CLI user the same token or the operator’s workspace.
| Question | Answer |
|---|---|
| Do all CLI users share one opo_… token? | No. Each login mints a new token tied to that user’s authorization row. |
| Can User B’s token read User A’s workspace? | No. The API resolves workspace only from the token’s authorization; another user’s channels and posts are unreachable. |
| Can a Solo user get a token for the operator’s workspace without permission? | No. Approve requires an active user_organizations membership for the selected workspace. |
| Can someone use the client secret to call /public/* | No. The secret only exchanges OAuth codes; it is not accepted as a programmatic bearer token. |
Pick the correct workspace
On /oauth/authorize, the user must choose and confirm which workspace to authorize. If they are a member of multiple workspaces, the token applies only to the workspace they select — not to every workspace they belong to.
Endpoints
Auth server API (SERVER_URL)
| Method | Path | Description |
|---|---|---|
POST | /device/code | CLI starts the flow. Returns device_code, user_code, verification_uri. |
POST | /device/token | CLI polls with {"device_code": "..."} until completed. |
GET | /health | Health check. |
Browser steps
When BROWSER_ORIGIN equals SERVER_URL (typical local dev), the auth server serves these directly:
| Method | Path | Description |
|---|---|---|
GET | /device/verify | Code entry page. Supports ?code= prefill. |
POST | /device/verify | Validates code; redirects to OAuth authorize UI. |
GET | /device/callback | OAuth redirect target; exchanges code and stores token. |
When BROWSER_ORIGIN is the web app (OpenQuok production), the same behavior is exposed at:
| Method | Path | Host |
|---|---|---|
GET | /cli/device/verify | BROWSER_ORIGIN |
POST | /cli/device/verify | proxied to auth server |
GET | /cli/device/callback | proxied to auth server |
Postgres state model
The server is effectively stateless beyond one Postgres table (device_requests). Rows are deleted after the CLI retrieves the token, or on next access if expired (15 minutes).
CREATE TABLE device_requests (
device_code TEXT PRIMARY KEY,
user_code TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' or 'completed'
access_token TEXT,
api_url TEXT,
organization_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); Deployment and scaling
- Works anywhere: any platform that runs Node.js and can reach Postgres (VPS, Railway, Fly.io, Render, etc.).
- Horizontal scaling: safe as long as every instance shares the same DATABASE_URL, SERVER_URL, BROWSER_ORIGIN, and OAuth client credentials. No sticky sessions required.
- Serverless: use a managed Postgres connection string appropriate for high concurrency (pooling).
- Self-hosted production: deploy both agent/server and web; see Configuration - Agent → Common setup steps.