Porta v1.11.0
Skip to content

Database Schema ​

Last Updated: 2026-09-17

Porta uses PostgreSQL 16 as its primary data store. The schema is designed around multi-tenant isolation with organization-scoped data.

Entity Relationship Diagram ​

Tables ​

organizations ​

The tenant table. Every user, client, and data point traces back to an organization.

ColumnTypeDescription
iduuidPrimary key (generated)
nametextDisplay name
slugtextURL slug (unique)
statustextactive or suspended
is_super_adminbooleanWhether this is the super-admin org
logo_urltextBranding: logo URL
favicon_urltextBranding: favicon URL
primary_colortextBranding: primary color hex
company_nametextBranding: display name
custom_csstextBranding: CSS overrides
default_localetextDefault locale
default_login_methodstext[]Default login methods (NOT NULL)
created_attimestamptzCreation timestamp
updated_attimestamptzLast update (auto-trigger)

Constraints:

  • Partial unique index ensures at most one is_super_admin = true row

users ​

End-user accounts scoped to an organization.

ColumnTypeDescription
iduuidPrimary key
organization_iduuidFK → organizations
emailcitextEmail (unique per org, case-insensitive)
password_hashtextArgon2id hash
statustextactive, inactive, or automatically locked
given_nametextFirst name
family_nametextLast name
nicknametextNickname
picturetextProfile picture URL
phone_numbertextPhone number
phone_number_verifiedbooleanPhone verified
email_verifiedbooleanEmail verified
localetextUser locale
login_countintegerTotal login count
last_login_attimestamptzLast successful login
failed_login_countintegerFailed login attempts (resets on success). Used by account lockout.
last_failed_login_attimestamptzTimestamp of last failed login. Used for auto-unlock cooldown.
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

Constraints:

  • Unique: (organization_id, email)

applications ​

Deployment-global SaaS product definitions that scope clients, roles, and claims. Applications have no organization_id: organizations connect to them through clients. See the Core ownership model.

ColumnTypeDescription
iduuidPrimary key
nametextApplication name
slugtextURL slug (unique)
descriptiontextDescription
statustextactive or inactive
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

application_modules ​

Logical groupings within an application.

ColumnTypeDescription
iduuidPrimary key
application_iduuidFK → applications
nametextModule name
slugtextModule slug
descriptiontextDescription
statustextactive or inactive

clients ​

OIDC clients (public or confidential). Each client belongs to one organization and one global application.

ColumnTypeDescription
iduuidPrimary key
client_idtextOIDC client_id (unique)
organization_iduuidFK → organizations
application_iduuidFK → applications
client_nametextDisplay name
client_typetextconfidential or public
application_typetextweb, native, or spa
redirect_uristext[]Allowed redirect URIs
grant_typestext[]Allowed grant types
response_typestext[]Allowed response types
scopetextSpace-separated scopes
token_endpoint_auth_methodtextAuth method
cors_originstext[]CORS origins
require_pkcebooleanRequire PKCE
require_consentbooleanShow the consent page for this client
login_methodstext[]Override login methods (NULL = inherit)
statustextactive or inactive

client_secrets ​

Client secrets with Argon2id hashing and SHA-256 pre-hash.

ColumnTypeDescription
iduuidPrimary key
client_iduuidFK → clients
secret_hashtextArgon2id hash
sha256_hashtextSHA-256 hash for OIDC lookup
labeltextOptional label
statustextactive or revoked
created_attimestamptzCreation timestamp
revoked_attimestamptzRevocation timestamp

roles ​

RBAC roles scoped to an application.

ColumnTypeDescription
iduuidPrimary key
application_iduuidFK → applications
nametextRole name
slugtextRole slug
descriptiontextDescription

permissions ​

Permissions scoped to an application.

ColumnTypeDescription
iduuidPrimary key
application_iduuidFK → applications
nametextPermission name
slugtextPermission slug
descriptiontextDescription

role_permissions ​

Maps permissions to roles (many-to-many).

ColumnTypeDescription
role_iduuidFK → roles
permission_iduuidFK → permissions

user_roles ​

Maps roles to users (many-to-many).

ColumnTypeDescription
user_iduuidFK → users
role_iduuidFK → roles

claim_definitions ​

Custom claim definitions scoped to an application.

ColumnTypeDescription
iduuidPrimary key
application_iduuidFK → applications
nametextClaim name
slugtextClaim slug
claim_typetextstring, number, boolean, json
validation_rulesjsonbType-specific validation rules
descriptiontextDescription

user_claim_values ​

Custom claim values assigned to users.

ColumnTypeDescription
iduuidPrimary key
claim_definition_iduuidFK → claim_definitions
user_iduuidFK → users
valuejsonbThe claim value

system_config ​

Deployment-global operational values, with an existing 60-second process-local runtime cache.

ColumnTypeDescription
iduuidPrimary key
keyvarchar(255)Unique configuration identifier
valuejsonbNative integer or locale string for the public catalog; separate internal values may remain
value_typevarchar(20)Legacy storage category; public type/validation comes from code
descriptiontextStored description; public metadata comes from code
is_sensitivebooleanLegacy storage flag; does not admit a row into the public catalog
created_attimestamptzCreation timestamp
updated_attimestamptzLast update, maintained by the existing trigger

The closed editable catalog contains exactly 18 keys. All defaults, inclusive ranges, labels, descriptions, groups and application modes are defined in code; Admin list/get never enumerate arbitrary stored rows. Internal super_admin_user_id is not exposed. Bootstrap settings and secrets remain external.

Migration 030_global_configuration_catalog.sql writes the exact native defaults, overwrites canonical legacy values, deletes seven obsolete public keys and preserves internal rows. Its Down is intentionally a no-op. Configuration writes update existing rows only, atomically audit once, and clear the local runtime cache after commit. Other healthy instances refresh runtime policy on their next read within the 60-second cache lifetime; the five provider-startup lifetimes require restarting every instance. Existing artifact and counter expiries are not rewritten.

audit_log ​

Immutable audit trail of all administrative and security events.

ColumnTypeDescription
iduuidPrimary key
actiontextEvent type (e.g., organization.created)
entity_typetextEntity type
entity_iduuidEntity ID
actor_iduuidWho performed the action
actor_emailtextActor's email
organization_iduuidOrganization context
metadatajsonbAdditional event data
created_attimestamptzEvent timestamp

oidc_payloads ​

PostgreSQL storage for long-lived OIDC artifacts (access tokens, refresh tokens, grants).

ColumnTypeDescription
idtextArtifact ID
typetextModel type
payloadjsonbFull OIDC payload
grant_idtextAssociated grant ID
user_codetextDevice code (if applicable)
uidtextUnique identifier
expires_attimestamptzExpiration
consumed_attimestamptzConsumption timestamp

admin_sessions ​

PostgreSQL mirror of Redis OIDC sessions for admin listing, filtering, and revocation management.

ColumnTypeDescription
session_idvarchar(128)Primary key (OIDC session ID)
user_iduuidFK → users
client_iduuidFK → clients
organization_iduuidFK → organizations
grant_idvarchar(128)Associated grant ID
ip_addressinetClient IP address
user_agenttextClient user agent string
created_attimestamptzSession creation time
expires_attimestamptzSession expiry time
last_activity_attimestamptzLast activity timestamp
revoked_attimestamptzRevocation timestamp (NULL = active)

Indexes: Partial indexes on user_id, organization_id, client_id, and expires_at where revoked_at IS NULL for efficient active session queries.

branding_assets ​

Organization logo and favicon image storage.

ColumnTypeDescription
iduuidPrimary key
organization_iduuidFK → organizations (CASCADE)
asset_typevarchar(20)logo or favicon
content_typevarchar(50)MIME type (e.g., image/png)
databyteaBinary image data
file_sizeintegerFile size in bytes
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

Constraints:

  • Unique: (organization_id, asset_type) — one logo and one favicon per org
  • Check: asset_type IN ('logo', 'favicon')
  • Check: file_size > 0 AND file_size <= 524288 (max 512 KB)

two_factor_* ​

Two-factor authentication tables for TOTP secrets (AES-256-GCM encrypted), email OTP codes, and recovery codes (Argon2id hashed).

Extensions ​

Porta uses two PostgreSQL extensions:

  • pgcrypto — UUID generation via gen_random_uuid()
  • citext — Case-insensitive text type for email addresses

Auto-Updated Timestamps ​

All tables with an updated_at column use a database trigger (trigger_set_updated_at) that automatically sets updated_at = NOW() on every UPDATE.

Released under the MIT License.