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];