Skip to content

Data model

ProviderWhenNotes
SQLiteDev default, testsSingle-file, zero ops
PostgreSQLProductionUUIDs, JSONB columns, indexed for sync workloads

The provider is chosen from Database:Provider in configuration. Migrations are auto-applied on startup via db.Database.MigrateAsync(). A DesignTimeDbContextFactory is included for dotnet ef commands.

Most user-owned entities use a composite primary key (Id, UserId). All read and write paths must filter by UserId — never query by Id alone, because a colliding Id from another user would cross tenants.

This is enforced in the entity configuration and audited through code review; there is no global query filter automatically applied. Treat any new entity as UserId-scoped unless you have a specific reason not to.

EntityPKFKPurpose
PiaUserId (GUID)Auth principal
RefreshTokenId (GUID)UserIdToken rotation, max 10 per user
UserSettingsUserIdUserIdJSON settings blob
ServerTemplate(Id, UserId)UserIdCustom optimization templates
ServerProvider(Id, UserId)UserIdAI provider configurations
ServerSession(Id, UserId)UserIdOptimization history
ServerMemory(Id, UserId)UserIdContext / memory store
ServerTodo(Id, UserId)UserIdTodo items
SyncCursorId (GUID)UserIdPer-device sync cursor
SyncEventId (GUID)UserIdSync audit trail
ServerDeviceDeviceId (string)UserIdE2EE device registration (keys, status, fingerprint)
ServerWrappedUmk(UserId, DeviceId)UserIdUMK wrapped for a specific device via ECDH

The DTOs that travel over the wire — SyncTemplate, SyncProvider, SyncSession, SyncMemory, SyncTodo — live in Pia.Shared and have explicit, hand-written mappers to/from the entities. There is no AutoMapper.

When a user enables E2EE, the following columns on PiaUser are populated:

  • IsE2EEEnabled — boolean.
  • UmkVersion — bumps on UMK rotation.
  • RecoveryWrappedUmkCiphertext — UMK wrapped with the recovery-derived KEK.
  • RecoveryKdfSalt, RecoveryKdfMemory, RecoveryKdfTime, RecoveryKdfParallelism — Argon2id parameters.
  • RecoveryWrapVersion — bumps when the recovery code is rotated.

See E2EE architecture for how these are used.

RefreshToken rows store a hashed token plus issue / expiry timestamps. Rotation is performed on every /auth/refresh call: the old row is deleted, a new one inserted. A per-user cap of 10 tokens is enforced — when exceeded, the oldest is pruned. A background RefreshTokenCleanupService removes expired rows.