Skip to main content

Local Database Schema

The app uses WatermelonDB with a SQLite adapter for local-first storage. Schema version: 11.

All timestamps are stored as Unix milliseconds (number). All tables include an implicit id column (string UUID) managed by WatermelonDB.


Tables

peers

Stores both the current authenticated user and their peers/contacts.

ColumnTypeNullableNotes
idstringUUID (WatermelonDB managed)
usernamestringNo
first_namestringNo
last_namestringYes
is_onlinebooleanNo
emailstringYesCurrent user only
phone_numberstringYesCurrent user only
email_verifiedbooleanYesCurrent user only
phone_number_verifiedbooleanYesCurrent user only. Declared twice in schema.ts's column array — a source-level duplicate, harmless but worth removing
rolestringYes"admin" | "rescuer" | "user" — sourced from server on upsert
is_guestbooleanYesWhether the peer is a guest account
last_seen_atnumberYesUnix ms of last observed activity. Server source: UserActivity.last_active (via GET /user-utils/search-user/{id}, refreshed when the peer is offline); LAN fallback: stamped on mDNS online/offline. Drives the "Last seen …" header label.

guest_user

Stores guest user profile when the app is used without an account.

ColumnTypeNullableNotes
idstringUUID
first_namestringNo
last_namestringNo
usernamestringNo

conversations

A conversation groups messages between participants.

ColumnTypeNullableNotes
idstringUUID
typestringNo"direct" | "group" | "solo" (legacy admin-created two-person) | "sms" (GSM-relayed)
titlestringYesUsed for group conversations
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete

conversation_participants

Join table — which users belong to which conversation.

ColumnTypeNullableNotes
idstringUUID
conversationstringNoFK → conversations.id
userstringNoFK → peers.id
joined_atnumberNoUnix ms
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete

messages

Individual chat messages within a conversation.

ColumnTypeNullableNotes
idstringUUID
conversationstringNoFK → conversations.id
senderstringNoFK → peers.id
message_typestringNo"text" | "file" | "call_log" | "sms"
contentstringNoMessage text or file reference
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete
linked_message_idstringYesAdded schema v8. Self-referencing FK → messages.id for reply threads
is_encryptedbooleanYesAdded schema v9. Marks NaCl-box-encrypted content

message_receipts

Delivery and read status per message per user.

ColumnTypeNullableNotes
idstringUUID
messagestringNoFK → messages.id
userstringNoFK → peers.id
statusstringNoMessageStatusType — "sending" | "sent" | "not_sent" | "delivered" | "read"
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete

calls

Call session records.

ColumnTypeNullableNotes
idstringUUID
conversationstringNoFK → conversations.id
initiatorstringNoFK → peers.id — who started the call
call_typestringNo"audio" | "video" (CallType)
statusstringNo"completed" | "missed" | "rejected" (CallStatus)
start_timenumberNoUnix ms
end_timenumberYesUnix ms — null while call is active
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete

call_participants

Which users participated in each call.

ColumnTypeNullableNotes
idstringUUID
callstringNoFK → calls.id
userstringNoFK → peers.id
joined_atnumberNoUnix ms
left_atnumberYesUnix ms — null while still in call
created_atnumberNoUnix ms
updated_atnumberNoUnix ms
is_deletedbooleanNoSoft delete

Enum Reference

EnumValues
MessageType"text" | "file" | "call_log" | "sms"
MessageStatusType"sending" | "sent" | "not_sent" | "delivered" | "read"
CallType"audio" | "video"
CallStatus"completed" | "missed" | "rejected"
ConversationType"direct" | "group" | "solo" (legacy admin-created two-person) | "sms" (GSM-relayed)

The wire protocol and the database use different names for the read receipt: the WebRTC data-channel message is seen (see CONNECTION_MESSAGES.md), while the value persisted in message_receipts.status is "read". Don't assume the strings match across the two layers.


Relationships

conversations ──< conversation_participants >── peers
conversations ──< messages
messages ──< message_receipts >── peers
conversations ──< calls
calls ──< call_participants >── peers

Sync

WatermelonDB uses a pull/push sync pattern with the server:

  • GET /sync/pull?last_pulled_at=<ms>&schema_version=<n> — fetches changes since last sync. Page size is a server-side default (100 per table), not a client parameter; the client pages through has_more/next_cursor until every table is drained.
  • POST /sync/push — pushes local created/updated/deleted records

features/sync/api/sync.api.ts also sends a schema_version query param, but the server does not read it — the parameter is commented out in server/app/api/sync.py, so FastAPI discards it. See API.md.

All synced tables use is_deleted (soft delete) and updated_at for conflict resolution.

Schema and migrations: features/shared/core/database/schema.ts, features/shared/core/database/migrations.ts