Skip to main content

Data model

Scryon's relational store is Postgres. The schema is owned by Flyway migrations in scryon-backend/src/main/resources/db/migration/. All tables use UUID primary keys, created_at / updated_at timestamps, and per-user scoping.

Entity-relationship diagram

Tables at a glance

TablePurposeNotes
usersAuthenticated user accounts.external_user_id (Firebase UID) is unique.
call_recordsOne row per uploaded call.Drives the state machine. Indexed on (user_id, created_at desc).
call_artifactsOne piece of content stored in object storage.(call_id, artifact_type) unique.
action_itemsExtracted + user-created action items.Owner fields capture speaker + role + display name; priority / source added in V19.
contactsUser-owned address-book entries.See API · Contacts. Linked to calls via call_records.scryon_contact_id.
call_sentiment_summaryOne row per completed call, denormalising sentiment / tone for analytics.Backs GET /api/analytics/vibe.
user_voice_profilesOptional voiceprint per user.At most one row per user; consent_version tracks consent UX.
call_processing_eventsPipeline event log.High-cardinality; retention policy enforced by sweeper.
topup_purchasesAudit trail of top-up purchases.See Plans & billing. Not yet written by a real payment webhook — PlanUsageService.creditTopup exists but isn't wired to an endpoint.
feature_flagsAdmin-controlled runtime switches.Uncached reads by design — see Admin console.
admin_audit_logAppend-only trail of admin actions.Write-only from enforcement's perspective — never read by any enforcement path.

users

ColumnTypeNotes
iduuid PKServer-generated.
external_user_idtext UNIQUEFirebase UID, or local-dev.
emailtextNullable; sourced from Firebase claims. Also what AdminAuthorizationService checks against the allowlist.
display_nametextUsed by the speaker resolver.
fcm_tokentextDevice push token, registered via PATCH /api/users/me. Null until the client registers one. See Push notifications.
plantextFREE (default) or PRO. Added V24. See Plans & billing.
topup_minutes_balance / topup_transcripts_balanceintOne-time purchased/granted credit, never expires, drawn down before the base monthly allowance resets. Added V24/V25.
plans_intro_shown_attimestamptz, nullableGates the one-time post-login Plans intro screen on Android. Added V24.
account_statustextACTIVE (default) / SUSPENDED / DISABLED. Added V27. See Admin console § Account status.
account_status_reason / account_status_updated_at / account_status_updated_bytext / timestamptz / textSet together whenever an admin changes account_status. Added V27.
created_at / updated_attimestamptz

call_records

ColumnNotes
id (PK)The callId surfaced to clients.
user_id (FK)Owner.
titleFree-form. User-editable via PATCH /api/calls/{id} (AND-003).
notesUser freeform notes (AND-005 / BE-008, added V20). Distinct from the AI analysis — never generated by the LLM.
contact_name / contact_id / phone_number / organizationCounterparty metadata sourced from the Android call log at upload time. contact_id here is the platform ContactsContract._ID, unrelated to scryon_contact_id.
scryon_contact_idNullable FK-style link (no DB constraint) to contacts.id (added V18). Set by auto-assign at upload or by PATCH /api/calls/{callId}/contact.
directionINCOMING / OUTGOING / UNKNOWN.
recorded_atClient-supplied; otherwise upload time.
duration_secondsBest-effort.
statusState machine.
error_reasonShort opaque code on FAILED.
created_at / updated_at

call_artifacts

ColumnNotes
id (PK)
call_id (FK)
artifact_typeEnum: TEMP_AUDIO, DIARIZATION_JSON, RAW_TRANSCRIPT_JSON, NORMALIZED_TRANSCRIPT_JSON, ANALYSIS_JSON.
storage_keyLogical path in object storage. See Storage layout.
content_typeMIME type of the bytes.
byte_sizeTotal bytes.
created_at

action_items

ColumnNotes
id (PK)
call_record_id (FK)
title
description
due_datedate, may be null.
priorityLOW / MEDIUM / HIGH, nullable (added V19; null on rows extracted before it existed).
statusOPEN / IN_PROGRESS / DONE / DISMISSED. Renamed from the legacy PENDING / COMPLETED two-state model in V19 — old values are back-filled, not accepted on new writes.
sourceAI (pipeline-extracted) or MANUAL (user-created via POST /api/calls/{callId}/action-items). Added V19; back-filled to AI for pre-existing rows.
contact_idNullable, added V19 for future per-contact action-item queries. Partially indexed.
owner_speaker_id / owner_speaker_label / owner_display_name / owner_roleSet from ActionItemOwnerMapper.
source_segment_ids_jsonJSON array of source segment IDs.
source_textProvenance for explainability.
created_at / updated_at / completed_at

See API · Action items for the full request/response contract.

contacts

User-owned address-book entries, independent of Android's system contacts. Added V17.

ColumnNotes
id (PK)
user_idNo FK constraint (matches existing schema convention).
nameRequired. Case-insensitive match key for auto-assignment.
phone_number / email / notesOptional.
created_at / updated_at

See API · Contacts.

call_sentiment_summary

One row per completed call, denormalising the sentiment / tone fields from ANALYSIS_JSON so GET /api/analytics/vibe doesn't have to deserialise every artifact on every request. Added V21.

ColumnNotes
call_record_id (PK, FK)ON DELETE CASCADE from call_records.
user_idScope; indexed with recorded_at for the analytics window query.
recorded_atCopied from the call, for windowed trend queries.
sentiment_overall / user_sentiment / contact_sentimentCopied from Sentiment.overall / userSentiment.overall / contactSentiment.overall.
tone_overall / tone_formality / tone_energy / tone_paceCopied from Tone.
created_at

user_voice_profiles

ColumnNotes
id (PK)
user_id (FK, unique)
providere.g. pyannote.
model / model_versionProvenance.
embedding_jsonOpaque provider blob. Not a vector we own — we don't decode it.
consent_versionMatches SCRYON_VOICE_CONSENT_VERSION at create time.
sample_duration_secondsFor UX hints.
created_at / updated_at

call_processing_events

ColumnNotes
id (PK)
call_idFK-style, nullable for non-call events.
user_idScope.
stageEnum from ProcessingStage.
statusSTARTED, COMPLETED, FAILED, SKIPPED.
providerWhen applicable.
duration_msSet by ProcessingEventLogger at end of stage.
error_codeShort opaque code.
error_messageSanitized.
created_at

topup_purchases

Audit trail of top-up purchases (see Plans & billing). Added V24; transcripts_granted added V25.

ColumnNotes
id (PK)
user_idNo FK constraint (matches existing schema convention).
product_skue.g. topup_150min.
minutes_granted / transcripts_grantedCopied from the SKU's config at purchase time — a later price/grant change never rewrites history.
amount_paid_cents / currency
provider / provider_referencee.g. Play Billing / Stripe transaction id.
created_at

feature_flags

Admin-controlled runtime switches. Added V26. See Admin console.

ColumnNotes
flag_key (PK)text, not an enum — a new flag doesn't need a migration.
enabledboolean, defaults false.
updated_at / updated_byStamped on every PATCH /api/admin/feature-flags/{key}.

admin_audit_log

Append-only trail of every admin console action. Added V27. See Admin console § Audit log.

ColumnNotes
id (PK)
actor_emailThe admin who performed the action.
actionFLAG_TOGGLE / CREDIT_GRANT / ACCOUNT_STATUS_CHANGE / PLAN_CHANGE.
target_type"feature_flag" or "user".
target_idFlag key or user UUID, stored as text — no FK constraint.
detailsFree-form human-readable summary, not machine-parsed.
created_at

Conventions

  • All FKs are ON DELETE CASCADE when the child is owned (artifacts, events, action items).
  • No raw audio bytes are ever stored in Postgres — only object-storage keys.
  • Timestamps are UTC. Hibernate time_zone=UTC is set explicitly.
  • JSON columns use jsonb so we can index and query without serialisation overhead.

See Database migrations for how the schema evolves.