HostAgentics Docs

HostAgentics Authentication and Authorization

Last updated: 2026-08-06

This document describes how authentication and authorization work on the HostAgentics platform: the identity provider, the session model, organization roles, tenant isolation, the invite flow, and the role requirements for operations. It is an internal engineering document; customer-facing text must not claim capabilities beyond what is implemented here.

1. Identity provider: Better Auth

Authentication is implemented with Better Auth (`apps/api/src/auth.ts`), using its Drizzle adapter against the platform database. Three plugins are enabled:

  • **Magic link** — passwordless sign-in via emailed one-time links.
  • **Organization** — multi-tenant organizations with role-based access.
  • **Two-factor** — TOTP-based MFA with recovery codes, per user.
  • Email + password sign-in (with verification) and Google/GitHub OAuth are supported when credentials are configured (`AUTH_GITHUB_ID/SECRET`, `AUTH_GOOGLE_ID/SECRET`). Sessions, accounts, and verification tokens live in the `users`, `sessions`, `accounts`, and `verificationTokens` tables; MFA methods and TOTP secrets are stored as encrypted envelopes in `mfaMethods` — never plaintext.

    2. Session model

    Better Auth's session model applies: a session belongs to one user, carries an expiry, and is recorded with IP address and user agent for anomaly detection. Session revocation is handled by Better Auth's session lifecycle. `securityEvents` records suspicious activity (login throttling, MFA failures) and the API rate-limits auth endpoints. Users authenticate once per session; every protected request is then authorized server-side per organization (section 4) — authentication alone never authorizes cross-org access.

    3. Organizations and roles

    Every tenant is an `organization`. A user belongs to an organization through a `membership` with exactly one role:

    | Role | Typical permissions (resource: actions) |

    | --- | --- |

    | `owner` | Runtime read/write/action/delete; billing read/write; member read/invite/remove/update; secrets read/write; relay manage; support read/write |

    | `admin` | Runtime read/write/action; billing read/write; member read/invite/remove; secrets write; relay manage; support read/write |

    | `developer` | Runtime read/write/action; secrets write; relay manage; support read/write |

    | `operator` | Runtime read/action (operational actions, e.g. restart); support read/write; no secret or billing access |

    | `billing` | Billing read/write only |

    | `viewer` | Read-only access to the org's resources |

    Role checks are enforced with an access-control matrix (`createAccessControl` in `apps/api/src/auth.ts`) **and** re-checked in route handlers via `requireRole` in `apps/api/src/authz.ts`. The `owner` role is the only role that can delete runtimes and remove members; only `owner`/`admin` can change billing. `operator` can take operational actions (restart, pause, resume) but cannot modify secrets. These requirements are enforced server-side on every operation; the UI merely hides what the role cannot do.

    4. Organization isolation

    Isolation is enforced at the query layer, not by client cooperation:

  • Every protected handler resolves the caller's membership (`resolveMembership`) using the organization ID from the verified session — never from a client-supplied organization ID alone.
  • Runtime-scoped resources go through `assertRuntimeOwnership`: the runtime row must belong to the caller's organization, or the request is rejected with 403 and logged as `authz.cross_org_access_attempt` in `auditEvents`.
  • Backups are checked through `assertBackupOwnership`, which resolves the backup's runtime and then verifies runtime ownership.
  • Organizations are the boundary for billing, runtimes, secrets, Relay tokens, support requests, and notifications. A user in two organizations holds separate memberships and never sees data across the boundary.
  • 5. Invite flow

    Members are added by invitation:

    1. An `owner`/`admin` invites an email address with a target role. The system stores an `invitations` row with a **hashed** token and an expiry.

    2. The invite link (with the plaintext token, shown once) is sent via transactional email (Resend).

    3. On acceptance, the token is verified against the hash, the membership is created with the invited role, and `acceptedAt` is set. Expired invites are rejected; unaccepted invites can be revoked by the inviter.

    6. Role requirements per operation (summary)

    | Operation | Minimum role |

    | --- | --- |

    | View org resources, runtimes, logs | viewer |

    | Restart/pause/resume a runtime | operator |

    | Create/configure a runtime, write secrets | developer |

    | Manage Relay tokens | developer |

    | Manage invitations (invite/remove members) | admin |

    | Change billing, plans, payment method | admin |

    | Delete a runtime, remove members | owner |

    7. Secrets handling

    No plaintext secrets are stored in the identity or session tables. Passwords are hashed; MFA secrets and any stored key material are encrypted envelopes (see `docs/security.md` and `docs/secret-rotation.md`). Session tokens and invite tokens are stored hashed; plaintext values are displayed exactly once at issuance.

    8. Related documents

  • `docs/security.md` — threat model, session and key handling
  • `docs/secret-rotation.md` — rotation of per-runtime and platform secrets
  • `docs/database.md` — identity schema tables