Skip to main content

Data model

ContentsVision stores everything in a single SQLite database (storage/inventory.db) through SQLAlchemy 2's async ORM. The models live in backend/app/models/. This page is the reference for every table and relationship.

Entity relationships

Foreign keys vs implicit links

The parent-child links inside a claim (Room.claim_id, ProcessingJob.room_id, InventoryItem.job_id, ItemPhoto.item_id, StagedPhoto.job_id) are real SQLAlchemy ForeignKey columns. The ownership links team_id (on users and claims) and Claim.created_by_user_id are plain indexed strings, not database-level foreign keys. Team scoping is enforced in the application layer, not by the database.

Tables

teams

The organization. Auto-created on first login from the SSO token.

ColumnTypeNotes
idstring (UUID)Primary key.
created_atdatetime
external_idint, nullableThe AdjustSquare team id.
namestringTeam display name.
team_tokenstring, indexedThe team's Adjust Square API token, used as the bearer token when submitting claims.
credit_ratefloat, nullableDollars of revenue per credit, for the media-processing Slack notification. NULL (the default) means use the global DEFAULT_TEAM_CREDIT_RATE.

users

A person. Auto-provisioned on first login.

ColumnTypeNotes
idstring (UUID)Primary key.
created_atdatetime
external_idint, nullable, unique, indexedThe AdjustSquare user id; becomes the adjuster id on submission.
emailstring, unique, indexed
namestring
team_idstring, indexedThe owning team's id.
is_activebool
last_seen_atdatetime, nullable, indexedWhen the user last made an authenticated request. Stamped by get_current_user (at most once a minute per user); powers the admin "Active Users (live)" stat.
timezonestringIANA zone picked on Settings > Profile ("" = browser local). Display preference only; storage stays UTC.

component_visibility

Admin-controlled show/hide overrides for UI components (currently training). One row per override; resolution is team override > global override (team_id="") > the registry default in services/component_visibility.py (training defaults to HIDDEN). See GET /api/admin/components and the admin Teams tab.

ColumnTypeNotes
idstring (UUID)Primary key.
updated_atdatetime
componentstring, indexedKey from the component registry, e.g. training.
team_idstring, indexed"" = the global row; otherwise the team this override applies to.
visiblebool

claims

The top-level unit of work.

ColumnTypeNotes
idstring (UUID)Primary key.
created_at / updated_atdatetimecreated_at indexed.
claim_numberstring, indexed
date_of_lossdate
zip_codestring
policy_limit / policy_deductiblefloat
insured_name / adjuster_namestring
loss_type / coverage_type / claim_typestringHuman-readable; mapped to Adjust Square codes on submit.
team_idstring, indexedOwning team.
is_trainingbool, indexedTraining sandbox flag: uploads complete instantly with fixture items (no AI, no credits) and submission is a dry run. One per user, auto-created by the Training hub. See training.
created_by_user_idstring, indexedThe users.id of the member who created the claim. That user's external_id is the Adjust Square user id.
adjust_square_claim_keystringSet after the first submission.
adjust_square_claim_idintThe integer claim id on Adjust Square.
submitted_atdatetime, nullable
is_archivedbool, indexedSoft-hide flag. Archived claims drop out of the default GET /api/claims list. Nothing is ever deleted by hand; archiving is the only removal path. A claim that has been submitted (submitted_at set) is locked and cannot be archived.
archived_atdatetime, nullableWhen the claim was archived (null when active). The daily purge permanently deletes anything archived more than 30 days ago; restoring clears this.

rooms

A labeled bucket inside a claim.

ColumnTypeNotes
idstring (UUID)Primary key.
claim_idstring, FK to claims.id, indexed
created_at / updated_atdatetime
namestringe.g. "Master Bedroom".
is_archivedbool, indexedSoft-hide flag. Archived rooms drop out of the default room list and their items stop counting toward the claim. A room that contains any submitted item is locked and cannot be archived.
archived_atdatetime, nullableWhen the room was archived (null when active). Drives the 30-day purge.

processing_jobs

One upload and its processing run.

ColumnTypeNotes
idstring (UUID)Primary key.
room_idstring, FK to rooms.id, indexed
source_typestringvideo, photo, or audio.
filename / file_pathstringThe original file (or photos dir).
file_size_bytesint
duration_secondsfloatDrives video/audio billing.
loss_type / claim_numberstringCopied from the claim at creation.
statusstringpending, awaiting_grouping, extracting, transcribing, segmenting, analyzing, synthesizing, complete, error.
progress_pctint0-100, streamed to the UI.
current_stagestringHuman-readable current step.
error_messagetextSet on failure.
total_frames / rooms_detected / items_foundintProcessing metadata.
transcript / transcript_segments / transcript_languagetext / JSON / stringTranscription output.
room_segmentsJSONTime-chunk ranges.
completed_atdatetime, nullable
billed_cost_usdfloatCredits charged (name kept for backward compatibility).
billed_minutesfloatWhole minutes billed (video/audio).
photo_request_countintAI calls made (photo jobs).
billing_chargedboolWhether credits were spent up front against the external billing service (only when ENABLE_BILLING). false otherwise.
billing_txn_idstringThe billing service's transaction id for the up-front spend, when one was made.
billing_amountfloatCredits spent up front, so a refund can return the exact amount.
billing_refundedbooltrue once the up-front charge was refunded because processing failed.
recovery_attemptsintHow many times the self-healing sweep requeued this job after an interruption (restart, crash, hang). One retry is allowed; a second interruption fails and refunds the job. See troubleshooting.
ai_cost_usdfloatOur internal OpenRouter spend in dollars (token counts priced at the model's per-million rates), copied from the in-memory observability counters when the job finishes. Separate from customer credit billing. Jobs predating this stay at 0.
ai_input_tokens / ai_output_tokens / ai_api_callsintThe token counts and AI call count behind ai_cost_usd.

job_log_archives

The durable copy of a job's pipeline log lines. While a job runs its logs live in the in-memory ring buffer (see observability); when the job reaches complete or error the buffer is copied here so the detail survives restarts and stays readable on the Logs page. One row per job, upserted.

ColumnTypeNotes
job_idstring, FK to processing_jobs.idPrimary key.
created_atdatetime
logsJSONThe full list of structured log entries (ts, level, msg, stage, elapsed).

submissions

One "Send for Estimation" event for a claim (a claim can be submitted several times; later submissions add the new items to the same Adjust Square claim).

ColumnTypeNotes
idstring (UUID)Primary key; shown as the submission id in the UI.
claim_idstring, indexed
created_atdatetime
items_count / photos_countintWhat the submission carried.
adjust_square_claim_keystringThe portal claim it landed on.
is_resubmitboolFalse for the claim-creating first submission.
is_dry_runboolTraining claims: recorded, sent nowhere.
statusstringprocessing while background item/photo uploads run, then complete.
processing_secondsfloatEnd to end: request start to background finish.

api_request_logs

One outbound AI API call (OpenRouter), for the admin "API Requests" inspector on the Logs page. The request JSON is stored with base64 images redacted to size placeholders; the response is stored in full.

ColumnTypeNotes
idstring (UUID)Primary key.
created_atdatetime, indexed
job_idstring, indexedDerived from the per-job pipeline logger.
service / method / url / modelstringopenrouter, POST, the endpoint, and the model id.
status_code / duration_ms / attemptsint / float / intFinal outcome and total time across retries.
input_tokens / output_tokens / cost_usdint / int / floatFrom the response usage block (OpenRouter reports real cost).
request_json / response_jsonJSONRedacted request; full response.
errortextSet when the call ultimately failed.

inventory_items

A single line of the inventory.

ColumnTypeNotes
idstring (UUID)Primary key.
job_idstring, FK to processing_jobs.id, indexed
source_typestringCopied from the job.
item_namestringSearchable retail-style name.
quantityintIdentical items are grouped.
roomstringRoom name (denormalized for convenience).
conditionstringNew / Above Average / Average / Below Average.
age_years / age_monthsintSet by the AI only from a spoken age (video narration, audio). Always 0 from photos, and never estimated from appearance.
notestext
brand / model_number / serial_number / skustring
damage_typestringsmoke / water / heat / impact / mold.
estimated_valuefloat
confidencefloat0.0-1.0.
audio_evidence / visual_evidencetextWhat was said / what was seen.
audio_start_seconds / audio_end_secondsfloatThe recording slice (audio items).
photo_path / crop_photo_path / photo_context_path / source_photo_pathstringLegacy photo columns, kept as a safety net.
photo_frame_timefloatTimestamp in the video.
photo_framestext (JSON)All analyzed frame URLs.
reviewed / flagged / approvedboolHuman review state.
is_archivedbool, indexedSoft-hide flag. Archived items drop out of the inventory lists, counts, exports, and Adjust Square submission, but keep their item_number (no renumbering on archive) so unarchiving restores the exact slot. Submitted items cannot be archived. Archiving is the only way to remove an item; there is no manual delete.
archived_atdatetime, nullableWhen the item was archived (null when active). Drives the 30-day purge.
sort_orderint
item_numberint, indexedUnique per claim.
submittedbool, indexedSent to Adjust Square.
adjust_square_item_idintRemote item id after submission.
photosrelationshipThe item_photos rows (source of truth for images).

item_photos

Photos attached to a finished item. The source of truth for item imagery (the legacy photo_path columns on the item are kept for compatibility).

ColumnTypeNotes
idstring (UUID)Primary key.
item_idstring, FK to inventory_items.id, CASCADE delete, indexed
path / crop_path / context_pathstring
is_primaryboolThe primary thumbnail.
photo_orderint"order" is reserved in SQL, hence the name.

staged_photos

Raw uploaded photos waiting to be grouped before AI runs.

ColumnTypeNotes
idstring (UUID)Primary key.
job_idstring, FK to processing_jobs.id, CASCADE delete, indexed
filenamestringFile on disk under <storage>/<job_id>/photos/ (a synthetic orig_####.jpg sequence).
original_filenamestringThe photo's real upload name (e.g. IMG_1234.jpg). The on-disk filename is synthetic, so this is the only record of the real name; grouping's "Sort by name" re-orders on it. Blank for manual segments and for rows created before the column existed.
captured_atstringWhen the photo was taken (YYYY-MM-DDTHH:MM:SS), read from EXIF in the browser at upload (the re-encode strips EXIF, so it must be captured before then); falls back to the file's modified time. Powers grouping's "Sort by date". Blank when unknown and for manual segments.
group_indexint, indexedPhotos sharing a group become one item. Manual segments use a high range (>= 1_000_000) so they never collide with photo groups.
photo_orderint
deletedbool, indexedSoft-delete; file removed on Process.
is_segmentboolTrue for a crop the user drew from a parent photo during grouping (a known item the AI never re-splits).
quantityintUser-set quantity for a described or segment item (e.g. 4 for four boxed rolls of tape). 1 by default.
manual_namestringUser-typed name for the item (a whole photo / merged group, or a segment). When set, the AI is skipped for it (no request, no credits). Blank means the AI names it.
source_filenamestringThe whole photo a segment was cropped from, so "clear segments" can restore it.
bboxstringJSON [x, y, w, h] (normalized) of the box on the source photo, kept for re-editing.

activity_log

One row per meaningful action, powering the Settings activity log and its Restore button. Covered in observability.

ColumnTypeNotes
idstring (UUID)Primary key.
created_atdatetime, indexed
user_id / user_namestringWho did it.
action_typestringMachine code the restore logic dispatches on.
action_label / targetstringHuman-readable text.
entity_type / entity_idstringWhat was acted on.
snapshotJSON, nullableEverything needed to undo the action.
restorable / restored / restored_atbool / bool / datetimeRestore state.

training_progress

One row per user per training mission; the source of truth for points and badges (mission complete = badge earned). See training.

ColumnTypeNotes
idstring (UUID)Primary key.
created_at / updated_atdatetime
user_id / team_idstring, indexedWho is training, and their team (for the leaderboard).
mission_keystring, indexedCurriculum mission key, e.g. messy-claim.
statusstringin_progress or complete.
quiz_score / quiz_total / quiz_attemptsintBest quiz result and attempt count.
checks_passedJSONCheck keys that passed on the latest grading run.
check_attemptsint
pointsintBanked once at completion: mission base + quiz bonus.
completed_atdatetime, nullable

training_certificates

Issued certifications, publicly verifiable by code. See training.

ColumnTypeNotes
idstring (UUID)Primary key.
codestring, unique, indexedShareable verification code, e.g. CV-3F9A-C24D-71BE.
user_id / team_idstring, indexed
holder_name / team_namestringSnapshotted at issue time so the public page stays stable.
levelstringCertified Contents Specialist.
score_pct / pointsintOverall quiz accuracy and total points at issue time.
issued_atdatetime
revokedboolInvalidates the certificate while keeping the code resolvable.

How the schema is created and migrated

There is no migration tool (no Alembic). On startup, core/database.py's init_db():

  1. Calls Base.metadata.create_all to create any missing tables.
  2. Runs a list of idempotent ALTER TABLE ... ADD COLUMN statements, each wrapped in a try/except so an already-existing column is silently skipped. This is how new columns are added to existing databases.
  3. Runs a few one-time backfills (also guarded): assigning sequential item_numbers to old items per claim, repairing crop_photo_path for old photo items, and seeding item_photos rows for items that predate the multi-photo model.
Adding a column

To add a column: add it to the model and append an ALTER TABLE ... ADD COLUMN line in init_db(). create_all only creates whole new tables; it never alters an existing one, so without the ALTER line the column exists in the model but not in an already-deployed database.