azat / terry toggle. It is now a real
multi-user application:
- Google sign-in for every user (Better Auth — free, open-source, MIT).
- Two roles. Admins see and manage everything; members see only their own workflows.
- Invite-only access. A new Google account can authenticate but lands
pending — disabled until an admin approves it. The emails in
ADMIN_EMAILSare auto-promoted to admin on first sign-in (azat, terry). - Per-user monthly USD budgets, set by admins. Once a member’s month-to-date spend reaches their cap, new runs are refused.
- Account disable. Admins can disable any account; its sessions are revoked immediately.
- An admin section listing every user with role, status, budget, and month-to-date spend, plus inline controls for all of the above.
What’s changed
All work is inapps/frontend plus one small change in mother-ai.
Auth core
lib/auth/auth.ts— Better Auth server instance (Google provider, database sessions,adminplugin,budgetCentscustom field). AdatabaseHookscreate hook implements the invite-only gate.lib/auth/client.ts— browser auth client.lib/db.ts— the frontend’s first Postgres connection (server-only pooledpg).app/api/auth/[...all]/route.ts— Better Auth handler.lib/auth/schema.sql—user/session/account/verificationtables +auth_audit_log.lib/auth/backfill.sql— one-time reassignment of pre-auth workflows to the azat account.
middleware.ts— optimistic cookie gate on page routes.lib/auth/guards.ts—requireUser/requireAdmin(pages) andrequireApiUser/requireApiAdmin(route handlers).- Route group
app/(authenticated)/holds the app chrome and a server-siderequireUser()gate;app/(authenticated)/admin/addsrequireAdmin()./loginrenders outside the group. - Every
app/api/**BFF route is guarded — mother-ai routes via the shared proxy, the rest with explicit guards.
mother-ai:GET /v1/workflowsgained an optionalcreated_byfilter (src/routes.rs,src/models.rs).lib/auth/workflow-scope.ts—stampActorwrites identity onto outbound payloads server-side;requireWorkflowAccess/requireJobAccessenforce ownership via indexed reads onworkflow_runs/job_runs.
lib/auth/budget.ts— month-to-date spend fromjob_runs.cost_usd;enforceBudgetpreflights BFF write routes (HTTP 402 over budget).GET /api/me/budgetfeeds the settings panel.lib/auth/admin.ts,app/api/admin/users/**, andapp/(authenticated)/admin/users/— the user-management surface; every mutation is written toauth_audit_log.
apps/frontend/.env.example): DATABASE_URL,
BETTER_AUTH_SECRET, BETTER_AUTH_URL, GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET, ADMIN_EMAILS, DEFAULT_BUDGET_USD.
Impact scope
- Frontend — auth is required everywhere; the fake
azat / terrytoggle is gone. The frontend now holds a Postgres connection (server-side only). - mother-ai — additive only: a new optional query param, fully backwards-compatible. It keeps its API-key auth; the Next.js BFF is the per-user trust boundary (the browser cannot reach mother-ai directly).
- worker / infra — unchanged. Auth tables live in the existing Postgres.
- Not backwards-compatible: the app cannot be used signed-out. Pre-auth
workflows must be reassigned with
lib/auth/backfill.sqlafter azat’s first sign-in, or their owner won’t see them. - Defaults: new users are disabled until approved; budgets default to
DEFAULT_BUDGET_USD; admins are exempt from budget enforcement. The budget cap is soft — checked at submission, so a running job can still overshoot.
Tests
- Frontend:
cd apps/frontend && npx --no-install tsc --noEmit,npx --no-install biome check .,pnpm build. - mother-ai:
cd mother-ai && cargo check && cargo test. - Worker suite (
tests/) is unaffected — auth is frontend-side. - End-to-end (manual, needs
DATABASE_URL+ Google OAuth configured): sign in as a non-admin → pending screen; sign in as anADMIN_EMAILSaccount → full access; approve the pending user; create workflows as two users and confirm each sees only their own; drop a budget and confirm a 402; disable a user and confirm the session is revoked.