Browse documentation

A project database is one PostgreSQL instance with four schemas, declared in apps/server/prisma/schema.prisma:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  schemas  = ["auth", "storage", "platform", "public"]
}
SchemaContents
authUsers, sessions, and every Better Auth table
storageBuckets, objects, tokens, CORS, bucket webhooks, deletion jobs
platformSettings, webhooks, edge functions, realtime outbox, devices, project API keys, the Git repo link, schema migrations
publicYour tables — the ones you create in the SQL editor and expose over REST
Note

Platform tables are deliberately kept out of public so the auto-generated REST API and RLS policies operate on your application tables only.

Enums

EnumSchemaValues
UserStatusauthactive, deleted
UserRoleauthUSER, ADMIN
DevicePlatformplatformios, android, web
FileVisibilitystoragepublic, private
FileStatusstoragepending, finalizing, deleting, ready
BucketDeletionJobStatusstoragequeued, running, completed, failed

auth schema

ModelTablePurpose
UserusersIdentity. Own columns (role, status, username, firstName, imageUrl, provider ids) plus Better Auth core columns (name, image, emailVerified, twoFactorEnabled, phoneNumber, phoneNumberVerified). username is unique and assigned by the create hook when a sign-up supplies none
SessionsessionActive sessions: token, expiresAt, ipAddress, userAgent, activeOrganizationId
AccountaccountCredential and OAuth links. Passwords live here (password), not on User
VerificationverificationEmail/OTP verification values with expiresAt
JwksjwksSigning key pairs backing the published JWKS
TwoFactortwoFactorTOTP secret, backupCodes, failedVerificationCount, lockedUntil
OrganizationorganizationOrgs (slug unique, metadata)
MembermemberOrg membership with role (default member)
InvitationinvitationPending org invites, status, expiresAt, inviterId, optional teamId
Team / TeamMemberteam / teamMemberTeams inside an org
PasskeypasskeyWebAuthn credentials (credentialID, counter, deviceType, backedUp, transports, aaguid)
ApiKeyapikeyMachine keys: prefix, key, permissions, expiresAt, plus refill and per-key rate-limit columns (rateLimitMax, rateLimitTimeWindow, remaining, requestCount)
SsoProviderssoProviderPer-issuer SSO config (oidcConfig, samlConfig, domain, optional organizationId)

Deletes cascade from User to sessions, accounts, two-factor rows, memberships, passkeys, SSO providers and team memberships.

storage schema

ModelTablePurpose
FilefilesOne object. bucket + key unique, mime, size, visibility, status, uploadExpiresAt, url. ownerId is SetNull on user delete
BucketbucketsLogical bucket: slug unique, visibility, optional quotaBytes
BucketTokenbucket_tokensScoped bearer tokens with permissions[] and optional expiresAt
BucketCorsbucket_corsPer-bucket allowlists (allowedOrigins/Methods/Headers, maxAgeSec)
BucketWebhookbucket_webhooksPer-bucket event delivery targets
BucketDeletionJobbucket_deletion_jobsResumable force-delete: totalObjects, deletedObjects, attempts, leaseToken, leaseExpiresAt. bucketId is intentionally not a foreign key so a finished job survives its bucket

platform schema

ModelTablePurpose
AppSettingapp_settingsOne row per settings group; key is the group name, value the JSON payload, updatedById a nullable FK to User
WebhookwebhooksProject webhooks: url, events[], enabled
WebhookDeliverywebhook_deliveriesDelivery attempts: status, ok, durationMs, test, errorMessage
EdgeFunctionedge_functionsname/route unique, code, enabled, optional schedule cron, per-function memoryLimitMb / timeoutMs (clamped by global ceilings)
EdgeFunctionLogedge_function_logsPer-invocation status, durationMs, stdout, errorMessage
EdgeFunctionSecretedge_function_secretsName/value secrets injected into the sandbox
RealtimeOutboxrealtime_outboxTransactional realtime events written by the mutation itself: model, action, payloads, deleteAudience, status, attempts, lease columns
DevicedevicesPush tokens per user with platform
ProjectApiKeyproject_api_keysPublishable and secret project keys: type, prefix, tokenHash (unique, sha256 of the full token), token (clear text for publishable keys only, NULL for secret keys by CHECK constraint), revokedAt
GitRepoLinkgit_repo_linksSingle-row (id defaults to "default") GitHub link: encrypted accessToken and webhookSecret, owner/repo/branch, migrationsPath, and the last sync's status, message and SHA
SchemaMigrationschema_migrationsStudio-authored migrations: upSql, downSql, checksum, snapshot, snapshotChecksum, status (pending → applied → rolledback)

Why an outbox

Realtime events are written inside the same transaction as the data change, then drained by RealtimeOutboxSchedulerService. A crash between commit and broadcast therefore loses no event: the row is still pending and gets leased again. The lease columns (leaseToken, leaseExpiresAt) keep two workers from double-sending.

Job leases, twice

Both BucketDeletionJob and RealtimeOutbox use the same pattern — status + attempts + nextAttemptAt/availableAt + a lease. Long-running deletions and event drains resume after a restart instead of starting over or dropping work.

Migrations

  • Prisma migrations for the platform schemas live in apps/server/prisma/migrations and are applied with npm run prisma:deploy (prisma:migrate in development).
  • Migrations you author for your tables are a different mechanism: the SchemaMigration model above, reviewed and applied from Studio, with checksum drift detection against the reviewed SQL and a stored snapshot of the live schema at apply time.
  • apps/server/prisma/seed.ts seeds a development database (npm run db:seed).