Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Database Schema

Aero2 uses Cloudflare D1 (SQLite at the edge) for all persistent data. The schema is defined in migrations/0001_schema.sql as a single fresh-start migration.

Entity Model

The schema is organized into four groups: core platform, authentication & OAuth, RBAC, and email verification.

Every tenant-scoped table includes an app_id column referencing applications(id) with ON DELETE CASCADE. See Multi-Tenancy Design for isolation guarantees.

Entity Relationship Diagram

applications (platform root)
├── application_api_keys      (API access per app)
├── users                     (one user pool per app)
│   ├── user_identity_links   (links to external IdPs)
│   ├── user_sessions         (active sessions)
│   ├── user_roles            (RBAC assignments)
│   ├── authorization_codes   (OAuth auth codes)
│   └── refresh_tokens        (OAuth refresh tokens)
├── identity_providers        (external OAuth/OIDC configs)
├── oauth_clients             (registered OAuth clients)
├── oauth_state               (CSRF state for OAuth flows)
├── audit_logs                (security event log)
├── roles                     (RBAC role definitions)
│   └── role_permissions      (role → permission mappings)
├── permissions               (granular permission definitions)
└── email_verification_codes  (email code / magic link / password reset)

Core Tables

applications

Central multi-tenancy table. Each row represents one application served on its own subdomain.

ColumnTypeDescription
idTEXT PKUUID identifier
slugTEXT UNIQUE NOT NULLAuto-generated subdomain slug (e.g., swift-mapleswift-maple.aero2.dev)
nameTEXT NOT NULLDisplay name
owner_org_idTEXTOwning developer team (organization in dashboard app)
logo_urlTEXTBranding: logo URL
favicon_urlTEXTBranding: favicon URL
primary_colorTEXTBranding: primary color hex
support_emailTEXTSupport contact email
homepage_urlTEXTApplication homepage
privacy_policy_urlTEXTPrivacy policy link
terms_urlTEXTTerms of service link
settingsTEXT NOT NULL DEFAULT ''JSON: signup_mode, mfa_policy, session_ttl, auth_methods, etc.
is_dashboardINTEGER NOT NULL DEFAULT 0Whether this is the built-in dashboard app
custom_domainTEXT UNIQUECustom domain (e.g., auth.myapp.com)
custom_domain_verifiedINTEGER NOT NULL DEFAULT 0Domain verification status
cf_custom_hostname_idTEXTCloudflare Custom Hostname ID
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

application_api_keys

API keys for programmatic access. Each key is stored as a prefix + HMAC hash.

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKOwning application
key_typeTEXT NOT NULLpublishable or secret
key_prefixTEXT NOT NULLFirst 12 chars for identification (e.g., pk_live_a1b2)
key_hashTEXT NOT NULLHMAC-SHA256 hash of full key
nameTEXTHuman-readable key name
last_used_atDATETIMELast API call timestamp
expires_atDATETIMEExpiration (NULL = never expires)
created_atDATETIMECreation timestamp

users

Core user table. Each user belongs to exactly one application.

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKOwning application
emailTEXT NOT NULLUser email address
email_verifiedINTEGER NOT NULL DEFAULT 0Email verification status
nameTEXTDisplay name
given_nameTEXTFirst name
family_nameTEXTLast name
pictureTEXTProfile picture URL
disabledINTEGER NOT NULL DEFAULT 0Account disabled flag
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

Unique constraint: UNIQUE(app_id, email) — same email can exist in different apps.

audit_logs

Immutable security event log for all authentication and administrative actions.

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKApplication context
user_idTEXT FK(users) ON DELETE SET NULLActing user (NULL for system/failed events)
event_typeTEXT NOT NULLEvent type (login, logout, login_failed, etc.)
event_dataTEXTJSON with event-specific details
ip_addressTEXTClient IP address
user_agentTEXTClient user agent
created_atDATETIMEEvent timestamp

Authentication & OAuth Tables

identity_providers

Configuration for external OAuth2/OIDC identity providers (GitHub, Google, etc.).

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKOwning application
nameTEXT NOT NULLProvider name (e.g., github, google)
typeTEXT NOT NULLoauth2 or oidc
authorization_endpointTEXT NOT NULLOAuth authorize URL
token_endpointTEXT NOT NULLOAuth token URL
userinfo_endpointTEXTUserInfo URL (optional for OIDC)
jwks_uriTEXTJWKS URL for OIDC token verification
client_idTEXT NOT NULLOAuth client ID
client_secretTEXT NOT NULLOAuth client secret (AES-256-GCM encrypted)
scopesTEXT NOT NULLJSON array of scopes
enabledINTEGER NOT NULL DEFAULT 1Whether provider is active
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

Unique constraint: UNIQUE(app_id, name)

user_identity_links

Links users to external identity provider accounts. A user can be linked to multiple IdPs.

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKApplication (explicit for query safety)
user_idTEXT NOT NULL FK(users)The local user
idp_idTEXT NOT NULL FK(identity_providers)The identity provider
idp_user_idTEXT NOT NULLUser ID at the IdP
idp_usernameTEXTUsername at the IdP
access_tokenTEXTEncrypted access token from IdP
refresh_tokenTEXTEncrypted refresh token from IdP
created_atDATETIMECreation timestamp

Unique constraint: UNIQUE(user_id, idp_id, idp_user_id)

oauth_clients

OAuth clients registered for the OIDC Provider functionality.

ColumnTypeDescription
idTEXT PKInternal UUID identifier
app_idTEXT NOT NULL FKOwning application
client_idTEXT NOT NULLPublic OAuth client identifier (client_ prefix)
client_secretTEXT NOT NULLPBKDF2-hashed secret
nameTEXT NOT NULLClient display name
redirect_urisTEXT NOT NULLJSON array of allowed redirect URIs
scopesTEXT NOT NULLJSON array of allowed scopes
grant_typesTEXT NOT NULLJSON array of allowed grant types
enabledINTEGER NOT NULL DEFAULT 1Whether client is active
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

Unique constraint: UNIQUE(app_id, client_id)

authorization_codes

Short-lived authorization codes for the Authorization Code Flow (10-minute TTL).

ColumnTypeDescription
codeTEXT PKThe authorization code
app_idTEXT NOT NULL FKApplication context
client_idTEXT NOT NULL FK(oauth_clients.client_id)Requesting client
user_idTEXT NOT NULL FK(users)Authenticated user
redirect_uriTEXT NOT NULLRedirect URI for this request
scopesTEXT NOT NULLJSON array of granted scopes
code_challengeTEXTPKCE code challenge
code_challenge_methodTEXTS256 or plain
nonceTEXTOIDC nonce for ID token
expires_atDATETIME NOT NULLCode expiration
created_atDATETIMECreation timestamp

refresh_tokens

Long-lived refresh tokens for token rotation.

ColumnTypeDescription
tokenTEXT PKHMAC-hashed token value
app_idTEXT NOT NULL FKApplication context
client_idTEXT NOT NULL FK(oauth_clients.client_id)Owning client
user_idTEXT NOT NULL FK(users)Owning user
scopesTEXT NOT NULLJSON array of granted scopes
expires_atDATETIME NOT NULLToken expiration
created_atDATETIMECreation timestamp

user_sessions

Tracks active user sessions for management and revocation.

ColumnTypeDescription
idTEXT PKSession identifier
app_idTEXT NOT NULL FKApplication context
user_idTEXT NOT NULL FK(users)Session owner
token_hashTEXT NOT NULLHMAC hash of session cookie
idp_nameTEXTWhich IdP authenticated this session
ip_addressTEXTClient IP at session creation
user_agentTEXTClient user agent string
created_atDATETIMESession creation time
expires_atDATETIME NOT NULLSession expiration
last_active_atDATETIMELast activity timestamp
revoked_atDATETIMERevocation timestamp (NULL if active)

oauth_state

CSRF protection state for OAuth flows. Entries are consumed atomically on callback.

ColumnTypeDescription
stateTEXT PKRandom state value
app_idTEXT NOT NULL FKApplication context
idp_nameTEXT NOT NULLTarget IdP name
redirect_uriTEXT NOT NULLPost-auth redirect URI
created_atDATETIMECreation timestamp
expires_atDATETIME NOT NULLState expiration (10 minutes)

RBAC Tables

roles

Role definitions. Each application has its own independent set of roles.

ColumnTypeDescription
idTEXT PKRole identifier (human-readable, e.g., operator)
app_idTEXT NOT NULL FKOwning application
nameTEXT NOT NULLRole name
descriptionTEXTHuman-readable description
created_atDATETIMECreation timestamp
updated_atDATETIMELast update timestamp

Unique constraint: UNIQUE(app_id, name)

permissions

Granular permission definitions following a resource:action naming convention.

ColumnTypeDescription
idTEXT PKPermission identifier (e.g., apps:create)
app_idTEXT NOT NULL FKOwning application
nameTEXT NOT NULLPermission name (same as id by convention)
resourceTEXT NOT NULLResource name (e.g., apps, users, clients)
actionTEXT NOT NULLAction name (e.g., read, write, delete)
descriptionTEXTHuman-readable description
created_atDATETIMECreation timestamp

Unique constraint: UNIQUE(app_id, name)

role_permissions

Many-to-many mapping between roles and permissions.

ColumnTypeDescription
app_idTEXT NOT NULL FKApplication (explicit for query safety)
role_idTEXT NOT NULL FK(roles)Role
permission_idTEXT NOT NULL FK(permissions)Permission
created_atDATETIMEAssignment timestamp

Primary key: (role_id, permission_id)

user_roles

Many-to-many mapping between users and roles.

ColumnTypeDescription
app_idTEXT NOT NULL FKApplication (explicit for query safety)
user_idTEXT NOT NULL FK(users)User
role_idTEXT NOT NULL FK(roles)Role
granted_byTEXTWho granted this role (bootstrap, system, or user ID)
granted_atDATETIMEAssignment timestamp

Primary key: (user_id, role_id)

Email Verification

email_verification_codes

Supports email code login, magic links, and password reset flows.

ColumnTypeDescription
idTEXT PKUUID identifier
app_idTEXT NOT NULL FKApplication context
emailTEXT NOT NULLTarget email address
code_hashTEXT NOT NULLHMAC hash of the verification code
typeTEXT NOT NULLemail_code, magic_link, or password_reset
expires_atDATETIME NOT NULLCode expiration
used_atDATETIMEConsumption timestamp (NULL if unused)
attemptsINTEGER NOT NULL DEFAULT 0Failed verification attempts (rate limiting)
created_atDATETIMECreation timestamp

Indexes

Indexes are designed around actual query patterns. UNIQUE constraints create implicit indexes and are not listed separately.

Hot-Path Indexes (Every Request)

IndexColumnsPurpose
idx_user_sessions_token(app_id, token_hash)Session cookie validation on every authenticated request
idx_user_sessions_user(app_id, user_id, revoked_at)Active session lookup for session management
idx_user_roles_app_user(app_id, user_id)RBAC permission check on every authorized request

Authentication Indexes

IndexColumnsPurpose
idx_users_email(email)Cross-app email lookup (admin search)
idx_idps_app_enabled(app_id, enabled)List active IdPs for login page
idx_oauth_clients_app_enabled(app_id, enabled)List active OAuth clients
idx_user_identity_links_app_user(app_id, user_id)User's linked identity providers
idx_user_identity_links_idp(idp_id)Find users by IdP (for IdP deletion checks)

Token & Code Indexes

IndexColumnsPurpose
idx_auth_codes_client(app_id, client_id)Authorization code lookup by client
idx_auth_codes_expires(expires_at)Expired code cleanup
idx_refresh_tokens_client(app_id, client_id)Refresh token lookup by client
idx_refresh_tokens_user(app_id, user_id)User's refresh tokens (revocation)
idx_refresh_tokens_expires(expires_at)Expired token cleanup
idx_oauth_state_expires(expires_at)Expired state cleanup
idx_user_sessions_expires(expires_at)Expired session cleanup

Admin & Dashboard Indexes

IndexColumnsPurpose
idx_applications_dashboard(is_dashboard)Find dashboard app quickly
idx_api_keys_app(app_id)List API keys for an app
idx_api_keys_prefix(key_prefix)Token validation by prefix
idx_audit_logs_app_created(app_id, created_at DESC)Paginated audit log listing
idx_audit_logs_app_user(app_id, user_id)Audit log filtered by user
idx_audit_logs_app_type(app_id, event_type)Audit log filtered by event type
idx_role_permissions_app(app_id)List permissions for an app's roles
idx_user_roles_role(role_id)Find users with a specific role

Email Verification Indexes

IndexColumnsPurpose
idx_verification_codes_lookup(app_id, email, type, used_at)Active code lookup
idx_verification_codes_expires(expires_at)Expired code cleanup

Bootstrap Process

On first HTTP request, ensureBootstrapped() runs idempotently (all inserts use INSERT OR IGNORE). It creates:

1. Dashboard Application

A built-in application with id = '00000000-0000-0000-0000-000000000000', slug = 'dashboard', and is_dashboard = 1. The schema seed also inserts this row.

2. Dashboard Roles

Role IDNameDescription
operatoroperatorPlatform operator with full system access
developerdeveloperDefault role for developer accounts
membermemberRead-only team member

3. Dashboard Permissions

Platform operations (operator only): platform:manage, developers:read, developers:write, apps:read:all, apps:write:all

Developer operations: apps:create, apps:read, apps:write, apps:delete, apps:transfer, teams:read, teams:write

Everyone: profile:read, profile:write

Legacy compatibility (seeded on dashboard for endpoint compatibility): clients:read, clients:write, clients:delete, users:read, users:write, users:delete, idps:read, idps:write, idps:delete, roles:read, roles:write

4. Role-Permission Mappings

  • operator gets all permissions (dashboard + legacy)
  • developer gets: apps:create, apps:read, apps:write, apps:delete, apps:transfer, teams:read, teams:write, profile:read, profile:write, users:read
  • member gets: profile:read, profile:write

5. API Keys

If no API keys exist for the dashboard app, generates one publishable and one secret key.

Role Assignment

When a user first authenticates:

  • If their email matches BOOTSTRAP_ADMIN_EMAIL and email is verified → assigned operator role
  • Otherwise → assigned developer role

Database Access Layer

The TypeScript DB layer lives in src/backend/db/:

db/
├── index.ts          # createDb(d1) — composes all query modules
├── types.ts          # Row types matching SQLite (booleans as number 0/1)
├── users.ts          # createUserQueries(db)
├── clients.ts        # createClientQueries(db)
├── sessions.ts       # createSessionQueries(db)
├── idps.ts           # createIdPQueries(db)
├── rbac.ts           # createRbacQueries(db)
├── audit.ts          # createAuditQueries(db)
├── applications.ts   # createApplicationQueries(db)
├── api-keys.ts       # createApiKeyQueries(db)
└── email-codes.ts    # createEmailCodeQueries(db)

Route handlers access the DB via:

const db = createDb(c.env.DB);
const user = await db.users.getById(appId, userId);
const roles = await db.rbac.getUserRoles(appId, userId);

Type convention: DB row types use number for booleans (SQLite stores 0/1). Route handlers normalize to boolean before returning to the frontend.

Migration Files

The schema uses a single consolidated migration file:

migrations/
  0001_schema.sql    # Complete schema for fresh installs

See Database Migrations for the migration workflow.