Skip to main content

huddle_core/storage/
schema.rs

1pub const MIGRATIONS: &[&str] = &[
2    // Our Ed25519 secret. olm_account_data left for forward compat but unused.
3    "CREATE TABLE IF NOT EXISTS identity (
4        id INTEGER PRIMARY KEY CHECK (id = 1),
5        ed25519_secret BLOB NOT NULL,
6        olm_account_data BLOB,
7        created_at INTEGER NOT NULL
8    );",
9    // Rooms we've created or joined
10    "CREATE TABLE IF NOT EXISTS rooms (
11        id TEXT PRIMARY KEY,
12        name TEXT NOT NULL,
13        creator_fingerprint TEXT NOT NULL,
14        encrypted INTEGER NOT NULL,
15        passphrase_salt BLOB,
16        created_at INTEGER NOT NULL,
17        last_active INTEGER
18    );",
19    // Per-room Megolm sessions: ours (outbound) and others' (inbound)
20    "CREATE TABLE IF NOT EXISTS room_megolm_sessions (
21        room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
22        sender_fingerprint TEXT NOT NULL,
23        session_id TEXT NOT NULL,
24        session_data BLOB NOT NULL,
25        is_outbound INTEGER NOT NULL,
26        created_at INTEGER NOT NULL,
27        PRIMARY KEY (room_id, sender_fingerprint, session_id)
28    );",
29    // Known members of each room, keyed by their fingerprint (the stable
30    // cryptographic identity). peer_id is informational and often unknown
31    // at the app layer, so it is not part of the primary key.
32    "CREATE TABLE IF NOT EXISTS room_members (
33        room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
34        peer_id TEXT NOT NULL DEFAULT '',
35        fingerprint TEXT NOT NULL,
36        last_seen INTEGER,
37        PRIMARY KEY (room_id, fingerprint)
38    );",
39    "CREATE TABLE IF NOT EXISTS room_messages (
40        id INTEGER PRIMARY KEY AUTOINCREMENT,
41        room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
42        sender_fingerprint TEXT NOT NULL,
43        direction TEXT NOT NULL CHECK (direction IN ('in', 'out')),
44        body TEXT NOT NULL,
45        sent_at INTEGER NOT NULL
46    );",
47    "CREATE INDEX IF NOT EXISTS idx_room_messages_room ON room_messages(room_id, sent_at);",
48    "CREATE INDEX IF NOT EXISTS idx_room_members_room ON room_members(room_id);",
49    // Peers we've manually dialed. We auto-reconnect on the next launch so
50    // the user doesn't have to retype an address to rejoin a room.
51    "CREATE TABLE IF NOT EXISTS known_peers (
52        address TEXT PRIMARY KEY,
53        label TEXT,
54        last_connected_at INTEGER,
55        last_attempt_at INTEGER,
56        created_at INTEGER NOT NULL
57    );",
58    // File attachments offered / received in a room. A row is created
59    // the moment we see a FileOffer; status moves through the lifecycle
60    // as chunks arrive and the user activates the card.
61    "CREATE TABLE IF NOT EXISTS room_attachments (
62        id INTEGER PRIMARY KEY AUTOINCREMENT,
63        room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
64        message_id INTEGER,
65        sender_fingerprint TEXT NOT NULL,
66        file_id TEXT NOT NULL,
67        name TEXT NOT NULL,
68        mime TEXT,
69        size_bytes INTEGER NOT NULL,
70        status TEXT NOT NULL CHECK (status IN ('offered','downloading','ready','saved','failed','cancelled')),
71        cache_path TEXT,
72        saved_path TEXT,
73        error TEXT,
74        encrypted INTEGER NOT NULL DEFAULT 0,
75        wrapped_key TEXT,
76        nonce TEXT,
77        megolm_session_id TEXT,
78        content_hash TEXT,
79        created_at INTEGER NOT NULL,
80        UNIQUE(room_id, file_id)
81    );",
82    "CREATE INDEX IF NOT EXISTS idx_room_attachments_room ON room_attachments(room_id);",
83    // Phase 5: contact verification — user marks a member's fingerprint
84    // as verified after comparing it out-of-band. Default 0 (unverified).
85    "ALTER TABLE room_members ADD COLUMN verified INTEGER NOT NULL DEFAULT 0;",
86    // Phase 6 QoL: per-room mute flag.
87    "ALTER TABLE rooms ADD COLUMN muted INTEGER NOT NULL DEFAULT 0;",
88    // Phase 6: display names — our own, plus per-room remembered names
89    // of other members.
90    "ALTER TABLE identity ADD COLUMN display_name TEXT;",
91    "ALTER TABLE room_members ADD COLUMN display_name TEXT;",
92    // Phase 0 (v0.3.0): app-level signed message envelopes. Members learn
93    // each others' pubkeys from `MemberAnnounce.sender_ed25519_pubkey`
94    // and persist them here so `SignedRoomMessage` envelopes can be
95    // verified without re-asking the network on every message.
96    "ALTER TABLE room_members ADD COLUMN ed25519_pubkey TEXT;",
97    // Phase A (v0.3.0): inbound-dial accept. Trusted=1 means an inbound
98    // connection from a peer with this fingerprint bypasses the prompt.
99    // Fingerprint is learned from Identify after the dial completes, so
100    // it's nullable on pre-Phase-A rows.
101    "ALTER TABLE known_peers ADD COLUMN fingerprint TEXT;",
102    "ALTER TABLE known_peers ADD COLUMN trusted INTEGER NOT NULL DEFAULT 0;",
103    // Phase A: a fingerprint the user has explicitly rejected. Inbound
104    // connections from a blocked fingerprint are auto-disconnected on
105    // every restart (the in-memory blocklist on its own would reset).
106    "CREATE TABLE IF NOT EXISTS blocked_peers (
107        fingerprint TEXT PRIMARY KEY,
108        blocked_at INTEGER NOT NULL
109    );",
110    // Phase B: soft owner role. 'owner' = can grant other owners and
111    // ban members; 'member' = vanilla participant. The creator of a
112    // room is auto-promoted at start_room time; subsequent grants
113    // come from `RoomMessage::OwnerGrant` (signed envelopes).
114    "ALTER TABLE room_members ADD COLUMN role TEXT NOT NULL DEFAULT 'member';",
115    // Phase B: per-room ban list. A banned fingerprint is ignored by
116    // honest clients — their MemberAnnounce is dropped, their messages
117    // skipped. The cryptographic enforcement is the immediate
118    // RotateRoomKey that follows a ban: the banned peer can't unwrap
119    // the new session key without the new passphrase.
120    "CREATE TABLE IF NOT EXISTS room_bans (
121        room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
122        banned_fingerprint TEXT NOT NULL,
123        banned_by_fingerprint TEXT NOT NULL,
124        signature_b64 TEXT NOT NULL,
125        banned_at INTEGER NOT NULL,
126        PRIMARY KEY (room_id, banned_fingerprint)
127    );",
128    "CREATE INDEX IF NOT EXISTS idx_room_bans_room ON room_bans(room_id);",
129    // Phase G: global per-fingerprint verification — populated when an
130    // SAS verification succeeds. Distinct from `room_members.verified`
131    // (which is per-room) so Phase E's global inbound filter can
132    // answer "is this fingerprint SAS-verified at all?" in one query.
133    "CREATE TABLE IF NOT EXISTS verified_peers (
134        fingerprint TEXT PRIMARY KEY,
135        verified_at INTEGER NOT NULL
136    );",
137    // Phase E: simple app-wide settings KV. First use: a global
138    // 'verified_only_inbound' flag that auto-rejects inbound dials
139    // from unverified fingerprints without prompting.
140    "CREATE TABLE IF NOT EXISTS app_settings (
141        key TEXT PRIMARY KEY,
142        value TEXT NOT NULL
143    );",
144    // Phase E: per-room verified-only join. When 1, existing members
145    // refuse to wrap their session key for an unverified joiner's
146    // MemberAnnounce, and the lowest-fp owner sends a signed
147    // `JoinRefused` so the joiner sees an explanation instead of a
148    // silent hang.
149    "ALTER TABLE rooms ADD COLUMN verified_only_join INTEGER NOT NULL DEFAULT 0;",
150    // Phase H: a flag for "we've shown the welcome-and-key-concepts
151    // onboarding card to this user". Persisted on identity (single
152    // row) so it doesn't reappear next launch.
153    "ALTER TABLE identity ADD COLUMN onboarding_seen INTEGER NOT NULL DEFAULT 0;",
154    // huddle 0.5: per-peer profile cache populated by signed
155    // ProfileUpdate broadcasts. `username = NULL` means the peer has
156    // explicitly cleared their username and should render as
157    // `[anonymous]`. `updated_at` is the sender's claimed monotonic ms;
158    // last-write-wins so an out-of-order replay can't downgrade a
159    // newer name.
160    "CREATE TABLE IF NOT EXISTS peer_profiles (
161        fingerprint TEXT PRIMARY KEY,
162        username TEXT,
163        updated_at INTEGER NOT NULL
164    );",
165    // huddle 0.7: explicit room kind ('direct' = 1-1 DM, 'group' = N-way
166    // room). Existing rooms back-fill to 'group' via the column default —
167    // they were created via the named `start_room` flow with group
168    // ergonomics from the start, so the back-fill is loss-free.
169    // RoomKind drives the sidebar split: DMs go in the Direct messages
170    // section, groups in Group rooms. Direct rooms also reject any
171    // MemberAnnounce that would push them past 2 members (honest-client
172    // enforcement) and are filtered out of third parties' discovery
173    // caches.
174    "ALTER TABLE rooms ADD COLUMN kind TEXT NOT NULL DEFAULT 'group';",
175];