These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Data model — User

The authenticated account holder. Implements JWTSubject (tymon/jwt-auth) so that role and subscription state are embedded directly in the token — the backend does not need a database lookup to authorize most requests. Uses SoftDeletes; deleted accounts are retained with deleted_at set.

Table: users

Core identity

ColumnTypeNullableDescription
idbigintnoPrimary key
namestringnoDisplay name
emailstringnoLogin credential (unique)
passwordstringnobcrypt hash (hidden from serialization)
avatar_urlstringyesCDN URL for profile avatar
avatar_keystringyesR2 storage key for the avatar file
email_verified_attimestampyesNull until email verification link is clicked
remember_tokenstringyesLaravel “remember me” token (hidden)

Role and subscription

ColumnTypeDefaultDescription
rolestringuseradmin or user
subscription_statusstringunpaidunpaid or paid
subscription_tierstringfreenone, free, bronze, premium, or custom
trial_ends_attimestampnullFree trial expiry (null if no trial)

Defaults are applied in the booted() hook, not the migration, so they are guaranteed regardless of how the model is created.

Paddle billing columns

ColumnTypeDescription
paddle_customer_idstringPaddle customer identifier
paddle_subscription_idstringActive subscription ID
paddle_price_idstringSubscribed price ID
paddle_subscription_statusstringPaddle-native subscription status string
paddle_last_payment_statusstringStatus of the most recent transaction
paddle_last_payment_failed_attimestampTimestamp of last failed payment
paddle_scheduled_cancellation_attimestampScheduled end-of-period cancellation
paddle_canceled_attimestampActual cancellation timestamp
paddle_cancellation_sourcestringinternal (user-initiated) or external (Paddle-initiated)
paddle_last_event_attimestampTimestamp of last processed Paddle webhook

These columns are populated and updated by the Paddle webhook handler. The authoritative subscription truth is the Paddle dashboard; these columns are a local cache.

Two-factor authentication

ColumnTypeDescription
two_factor_secretstringTOTP secret (application-level encryption via Laravel Crypt::encrypt, hidden from serialization)
two_factor_confirmed_attimestampWhen TOTP was confirmed; null means TOTP not active
two_factor_email_enabledbooleanWhether email-based 2FA is enabled
two_factor_recovery_codesarrayEncrypted recovery codes (hidden from serialization)

Account lifecycle

ColumnTypeDescription
account_deletion_requested_attimestampTimestamp of deletion request (soft-delete queue)
deleted_attimestampSoftDeletes field; null for active accounts
created_attimestamp
updated_attimestamp

Subscription tier ranks

Tiers are ordered by rank for canAccessSubscriptionTiers() gate checks:

TierRank
none0
free0
bronze1
premium2
custom3

admin users and custom-tier users always pass all tier gates regardless of rank. An account with subscription_status = unpaid fails tier checks above free unless it is admin or custom.

JWT custom claims

Every issued token embeds three custom claims beyond the standard sub / iat / exp:

{ "sub": 42, "role": "user", "subscription_status": "paid", "subscription_tier": "bronze" }

Token TTL is 20,160 minutes (14 days). There is no refresh endpoint — the frontend re-authenticates when a 401 is received. The JWT blacklist (database-backed) is enabled; logout invalidates the token immediately.

Auth payload (/me response)

toAuthPayload() shapes the object returned by GET /me and the login response. It includes the core identity fields, all Paddle subscription fields, 2FA status flags, and the permissions array (only populated for admin users: ['admin.access', 'users.manage']).

Relations

RelationTypePivot
ownedWorkspaces()belongsToMany Workspaceworkspace_user
ownedFinders()belongsToMany Finderfinder_user
ownedMaps()belongsToMany Mapmap_user
ownedSets()belongsToMany Setset_user
ownedLocations()belongsToMany Locationlocation_user
authSessions()hasMany AuthSession