Identity and authorization
Doso uses one authorization stack for every product instance:
- Session/JWT: Better Auth is canonical for human authentication,
organizations, teams, invitations, JWT issuance, and JWKS publication. The
web app mounts Better Auth at
/api/auth. Browser API calls obtain a Better Auth JWT from/api/auth/tokenand forward it to the Rust API asAuthorization: Bearer .... Local compose and hosted deploys both use this. - Local override: crate tests and the unadvertised Desktop embedded
fallback may set
DOSO_AUTH_MODE=localand use the loopback principal. That is not how you run compose or a hosted instance. Service-account bearer tokens (dsa_…) still work in both modes.
The Rust API remains the authorization boundary for Doso resources. It verifies
human JWTs through configured JWKS (DOSO_JWKS_URL, DOSO_AUTH_ISSUER,
DOSO_AUTH_AUDIENCE) and maps them into the generalized identity model.
Install-wide API keys are not supported; automation uses service-account bearer
tokens (dsa_…).
Authorization is enforced by the Casbin-backed policy layer in doso-core.
Routes are classified into generic resource kinds and actions, then evaluated
against subjects in the active scope. Workflow-specific rules must stay in
workflow definitions or policy data, not in the engine.
Service accounts are non-human principals for automation. They are created and
credentialed through the Rust API under /v1/service-accounts; raw credential
secrets are shown once at issuance and stored only as hashes server-side.
Deployment packages#
| Setup | Package | Auth |
|---|---|---|
| Compose | deploy/local | Better Auth + JWKS |
| Cloud | deploy/cloud/ | Better Auth + JWKS |
| Orgs | deploy/saas/ overlay | Better Auth orgs + JWKS + tenant-runtime |
| Desktop | surface via Connect to server | Auth of the instance it opens |
Canonical matrix: docs/product/deployment.md.
Better Auth is required for every HTTPS / server deploy, not only SaaS.
Hosted tenant runtime#
Hosted Doso deployments use crates/doso-tenant-runtime as a separate
control-plane service for platform operators. It owns tenant provisioning,
suspension, soft deletion, quota metadata, placement, migrations, and tenant
audit events in its own control-plane database.
Tenant creation composes doso-core by creating an organization Scope in the
data-plane DATABASE_URL through IdentityStore, then seeding organization
policies through AuthzEngine. Control-plane tenants mirror Better Auth
organizations: external_org_id is the Better Auth organization id (the same
value carried in JWT organization_id claims).
Bidirectional provisioning and edits:
- Sign-up creates a default Better Auth organization for the user; an
afterCreateOrganizationhook mirrors a control-plane tenant. Public sign-up is disabled whenDOSO_DISABLE_PUBLIC_SIGNUP=1. New default workspaces are capped byDOSO_SIGNUP_ORG_PER_HOUR(default 20). - Tenant admin creates/edits tenants via
/api/operator/tenants(operator bearer token): create, patch name/slug, suspend, activate, and delete keep Better Auth organizations and control-plane tenants aligned. Prefer these routes over calling tenant-runtime directly when Better Auth is in use. - Better Auth
afterUpdateOrganization/afterDeleteOrganizationhooks sync name/slug and soft-delete onto the control plane. Suspend/activate from admin also stamps organization metadatadoso_status.
Admin visibility#
GET /api/operator/tenants— control-plane list enriched with Better Authmember_count/ invitation counts.GET /api/operator/tenants/{id}/overview— members, teams, invitations, role histogram / organization roles, plus object counts (no content).GET /v1/tenants/{id}/usage(tenant-runtime) — data-planeCOUNT(*)byorganization_scope_id, with aby_userbreakdown keyed bycreated_by_subject(user:{betterAuthUserId}).
Data-plane creates stamp scope_id and created_by_subject from the request
principal (documents, conversations, workflow runs/defs, connections). Rows
created before that stamping remain on the local default scope and will not
appear in org usage until rewritten.
Direct POST /v1/tenants on tenant-runtime still works for control-plane-only
bootstrap; pass external_org_id when linking an existing IdP org, otherwise it
defaults to the tenant id. Create is idempotent on external_org_id. Tenant
lifecycle state does not branch the workflow engine; workflows continue to run
against generic scopes and policies.
The data plane looks up organization scopes from a verified JWT
organization_id claim and personal scopes when a JWT has no active
organization. An unknown organization is not provisioned on first data-plane
access and does not make the caller owner. Organization scopes and the known
owner are created by control-plane events (afterCreateOrganization and
operator create-tenant). Personal scopes may still be created for a human
without an organization. Better Auth JWTs are issued with RS256 so the Rust
JWKS verifier can validate them, and carry organization_id, roles,
team_ids, and sid only when a live membership lookup succeeds.
Suspended or deleted organization scopes fail closed on every data-plane request. Personal scopes stay active.
Operator routes are protected with DOSO_TENANT_OPERATOR_TOKEN bearer auth and
can be driven from the lightweight deploy/saas/tenant-admin/ frontend.
JWKS private-key encryption#
Better Auth encrypts JWKS signing private keys at rest with
BETTER_AUTH_SECRET (AES-256-GCM). Hosted/JWKS mode already refuses the
documented example secret.
- Rotation: configure versioned secrets (
BETTER_AUTH_SECRETS/ Better Authsecrets) so existing ciphertext still decrypts while new writes use the current key. - Recovery: if the secret is lost or leftover plaintext keys fail to
decrypt, delete the
jwkstable rows and restart. New encrypted key pairs are minted; existing JWTs fail verification until clients refresh.