Leave Localhost logoLeave LocalhostDocs
Architecture

Security Model

Leave Localhost implements a layered security model: authentication via Better Auth, authorization via role + capability checks, and step-up verification for sensitive actions.

Leave Localhost implements a layered security model: authentication via Better Auth, authorization via role + capability checks, and step-up verification for sensitive actions.

Authentication

Authentication is handled by Better Auth with the @convex-dev/better-auth adapter for Convex. The setup lives in packages/backend/convex/auth.ts.

Supported Methods

MethodPluginAlways available
Email + passwordBuilt-inYes
Magic link (passwordless)magicLinkYes (requires Resend)
Google OAuthBuilt-inYes
Microsoft OAuthBuilt-inWhen credentials configured
TOTP (2FA)twoFactorOpt-in per user

Session Management

Better Auth manages sessions via cookies. The Next.js proxy at apps/app/src/proxy.ts runs on every matched request and performs an early, cookie-based redirect guard: it checks for the presence of a Better Auth session cookie to decide where to route the request.

  • Unauthenticated users accessing protected routes are redirected to /login.
  • Authenticated users accessing auth pages (/login, /verify-2fa) are redirected to /.

This proxy check looks only at cookie presence, not validity. It is an early routing and UX optimization, not the authoritative auth layer — real session validation and authorization still happen in Convex queries, mutations, actions, and server-side route logic.

Next 16 naming: the request boundary lives in proxy.ts, which is the current Next.js 16 convention. The older middleware.ts name is deprecated; do not rename this file back to middleware.ts while the app is on Next 16.

Authorization

Authorization combines two layers:

1. Role-Based Access (Better Auth)

Every organization member has a role. Four roles are defined in auth/organizationAccess.ts:

RoleOrganizationMembersBillingFeatures
ownerread, update, deletefull CRUDread, manageall
adminread, updatefull CRUDreadall
memberreadreadall
viewerreadread

Roles are enforced via Better Auth's access control system. Each permission rule maps to a Better Auth statement like { organization: ["delete"] }.

2. Capability-Based Entitlements

Capabilities are granted by billing plans and tracked in the billing_grants table. Defined capabilities:

CapabilityGranted by
feature.proAny Pro plan
workspace.members.invitePro plans
workspace.members.limit.10Pro plans
workspace.members.limit.unlimited(reserved)
billing.portalPro plans
usage.ai.generate(reserved)

How Permission Checks Work

Every protected function calls requireAppPermission() or canAppPermission(). The check evaluates three conditions in order:

  1. Actor resolution — is the user authenticated, does the app user record exist, and is the user a member of the target organization?
  2. Role check — does the member's role satisfy the Better Auth permission requirement?
  3. Capability check — does the organization have the required capabilities (from active billing grants)?
  4. Resource policy — if the permission rule specifies one, does the additional business rule pass? (e.g., cannotRemoveLastOwner, memberLimitNotExceeded)
// Example: require billing.read permission
const actor = await requireAppPermission(ctx, {
  permission: "billing.read",
});

App Permission Keys

All permission keys and their rules are defined in permissions/appPermissions.ts:

PermissionRole requirementCapabilitiesResource policy
organization.readorganization: ["read"]
organization.updateorganization: ["update"]organizationMustBeActive
organization.deleteorganization: ["delete"]organizationMustBeActive
member.readmember: ["read"]
member.invitemember: ["create"]workspace.members.invitememberLimitNotExceeded
member.updateRolemember: ["update"]cannotModifyOwnerUnlessOwner
member.removemember: ["delete"]cannotRemoveLastOwner
billing.readbilling: ["read"]
billing.managebilling: ["manage"]
feature.pro.usefeature: ["pro.use"]feature.pro

Resource Policies

Four resource policies add context-aware rules:

  • organizationMustBeActive — blocks operations on suspended/deleted orgs.
  • cannotRemoveLastOwner — prevents removing the only owner.
  • cannotModifyOwnerUnlessOwner — only owners can change an owner's role.
  • memberLimitNotExceeded — blocks invites when the plan's member limit is reached.

Step-Up Verification

Destructive or high-risk actions require step-up verification. This is a separate layer that sits above normal permissions.

Risk Levels

LevelPolicyExamples
0No step-up(Reserved for custom actions)
1Recent sign-in (30 min)Open billing portal
2Recent sign-in, or password/email codeRemove a member
3Password, email code, or TOTP onlyChange password, cancel subscription
4Same as 3, single-use grant, 5 min TTLDelete account, delete organization

Sensitive Actions Catalog

Action IDRiskOrg-scoped
account.delete4No
account.changeEmail3No
account.changePassword3No
account.disableTwoFactor3No
account.regenerateBackupCodes3No
organization.delete4Yes
organization.changeMemberRole3Yes
organization.removeMember2Yes
billing.cancelSubscription3Yes
billing.openPortal1Yes

Verification Flow

  1. The client calls the protected mutation/action.
  2. The function calls requireSensitiveAction().
  3. If no valid grant exists, a SENSITIVE_VERIFICATION_REQUIRED error is thrown with metadata about which methods are accepted.
  4. The client shows a verification dialog (password, email code, or TOTP).
  5. The user submits the verification, which creates a time-limited grant.
  6. The client retries the original action — the grant satisfies the check.

Security Properties

  • Verification codes are salted and peppered before hashing; raw codes are never stored.
  • Grants are purpose-specific, organization-scoped (when applicable), session-scoped, and optionally single-use.
  • Expired challenges and grants are pruned by the hourly cron job.

Next Reads

On this page