Auth and organizations
The tenant boundary. How sign-in works, how every resource is scoped to an organization, and how the dev-mode switches behave.
Every resource in UseAgent, a run, a secret, a skill, a piece of knowledge, an integration, belongs to exactly one organization. That boundary is enforced by the backend, not trusted from the client.
Sign-in
/login and /signup go through the better-auth surface mounted at
/api/auth/* (GET and POST), which owns sessions, email/password sign-in,
and the organization plugin. Google social sign-in is env-gated: it is offered
only when both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set.
GET /api/config advertises which methods are enabled
(auth: { google, emailPassword }), so the UI renders the right affordances
without probing.
Self-service account creation is a development convenience, not a production
surface. Signup is disabled for both email/password and Google whenever verified
dev mode is off, and the check always fails closed when NODE_ENV=production,
even if a dev-mode flag was accidentally left on. Production users are
administratively provisioned. Every newly created user, however provisioned,
receives a personal organization on creation so they land in their own tenant.
Organization scoping
The organization is the tenant boundary, and scope is resolved server-side on every request; the backend never trusts an organization id supplied by the client. Resolution follows a fixed ladder:
- A valid session resolves to its user plus the session’s active organization, falling back to the user’s first membership.
- An authenticated user with zero memberships gets
403 no_organization. It never silently borrows the dev org, because that would cross tenancy. - An anonymous request gets
401 unauthorized, unless the dev-org fallback is allowed (below), in which case it resolves to the seeded dev org.
Every /api/* path runs behind a universal fail-closed adapter: a route is
org-scoped unless its path is explicitly public or self-authenticating (health,
config, the better-auth surface, the signature-verified Slack ingress, and the
capability-authenticated internal bridges). A new router is therefore protected
by default; forgetting a per-router guard does not leave it open.
Roles
Membership role is a real authorization tier. Routes wrapped in the org-admin
guard require the caller’s membership role to be owner or admin; a plain
member gets 403 organization_admin_required. The gated surfaces are:
PUTandDELETE /api/secrets/:name, because secrets are executable sandbox inputs and rotating credentials is an administration operation.- Accept and dismiss for learning-lane knowledge drafts and skill proposals, because they change (or decline to change) what the whole organization learns.
Dev mode and the dev-org fallback
A development mode exists to ease local work. The dev-org fallback, where an
anonymous request resolves to the seeded dev org, defaults on in dev and off in
production; ALLOW_DEV_ORG is the explicit switch in either direction. Set
ALLOW_DEV_ORG=0 to force real auth even in dev, or ALLOW_DEV_ORG=1 to open
the fallback deliberately in any environment. Self-signup is stricter: it is
hard fail-closed in production regardless of any dev-mode flag.