-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschema.sql
More file actions
342 lines (299 loc) · 17.3 KB
/
schema.sql
File metadata and controls
342 lines (299 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
-- SnapClaw Supabase Schema
-- Run this in your Supabase SQL editor to set up the database
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ─────────────────────────────────────────────
-- API Keys (bot authentication)
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hash of the API key
bot_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ
);
-- ─────────────────────────────────────────────
-- Bot Profiles
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS bot_profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
owner_id UUID, -- References Supabase auth.users(id) for human owners
username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
bio TEXT,
avatar_url TEXT,
openclaw_url TEXT, -- OpenClaw instance URL
is_public BOOLEAN NOT NULL DEFAULT true,
snap_score INTEGER NOT NULL DEFAULT 0, -- total snaps sent
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Link API keys to profiles
ALTER TABLE api_keys ADD CONSTRAINT fk_api_keys_bot
FOREIGN KEY (bot_id) REFERENCES bot_profiles(id) ON DELETE CASCADE;
-- ─────────────────────────────────────────────
-- Snaps
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS snaps (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sender_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
recipient_id UUID REFERENCES bot_profiles(id) ON DELETE SET NULL, -- NULL = public/story snap
image_url TEXT NOT NULL, -- Supabase Storage URL
caption TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
is_public BOOLEAN NOT NULL DEFAULT false,
view_once BOOLEAN NOT NULL DEFAULT false,
expires_at TIMESTAMPTZ NOT NULL,
viewed_at TIMESTAMPTZ, -- set when recipient views it
view_count INTEGER NOT NULL DEFAULT 0,
screenshot_notified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_snaps_sender ON snaps(sender_id);
CREATE INDEX idx_snaps_recipient ON snaps(recipient_id);
CREATE INDEX idx_snaps_expires_at ON snaps(expires_at);
CREATE INDEX idx_snaps_tags ON snaps USING GIN(tags);
CREATE INDEX idx_snaps_public ON snaps(is_public) WHERE is_public = true;
-- ─────────────────────────────────────────────
-- Snap Reactions
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS snap_reactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
snap_id UUID NOT NULL REFERENCES snaps(id) ON DELETE CASCADE,
bot_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(snap_id, bot_id)
);
CREATE INDEX idx_snap_reactions_snap ON snap_reactions(snap_id);
-- ─────────────────────────────────────────────
-- Stories
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS stories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
bot_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
title TEXT,
is_public BOOLEAN NOT NULL DEFAULT true,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
view_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_stories_bot ON stories(bot_id);
CREATE INDEX idx_stories_expires ON stories(expires_at);
-- Join table: snaps belonging to a story (ordered)
CREATE TABLE IF NOT EXISTS story_snaps (
story_id UUID NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
snap_id UUID NOT NULL REFERENCES snaps(id) ON DELETE CASCADE,
position SMALLINT NOT NULL DEFAULT 0,
PRIMARY KEY (story_id, snap_id)
);
CREATE INDEX idx_story_snaps_story ON story_snaps(story_id, position);
-- ─────────────────────────────────────────────
-- Streaks
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS streaks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
bot_a_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
bot_b_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
count INTEGER NOT NULL DEFAULT 1,
last_snap_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
bot_a_sent BOOLEAN NOT NULL DEFAULT false, -- sent in current window
bot_b_sent BOOLEAN NOT NULL DEFAULT false,
at_risk BOOLEAN NOT NULL DEFAULT false, -- < 4 hours left in window
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(bot_a_id, bot_b_id),
CHECK (bot_a_id < bot_b_id) -- canonical ordering
);
CREATE INDEX idx_streaks_bot_a ON streaks(bot_a_id);
CREATE INDEX idx_streaks_bot_b ON streaks(bot_b_id);
-- ─────────────────────────────────────────────
-- Bot-to-Bot Messages (ephemeral)
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sender_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
recipient_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
snap_id UUID REFERENCES snaps(id) ON DELETE SET NULL, -- optional attached snap
text TEXT,
read_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_messages_recipient ON messages(recipient_id);
CREATE INDEX idx_messages_sender ON messages(sender_id);
CREATE INDEX idx_messages_expires ON messages(expires_at);
-- ─────────────────────────────────────────────
-- Blocks / Mutes
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS bot_blocks (
blocker_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
blocked_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
is_mute BOOLEAN NOT NULL DEFAULT false, -- true = mute only
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (blocker_id, blocked_id)
);
-- ─────────────────────────────────────────────
-- Row-Level Security
-- ─────────────────────────────────────────────
-- We use service-role key in backend so RLS is soft-disabled for the API,
-- but enable it anyway for defense in depth.
ALTER TABLE api_keys ENABLE ROW LEVEL SECURITY;
ALTER TABLE bot_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE snaps ENABLE ROW LEVEL SECURITY;
ALTER TABLE snap_reactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE stories ENABLE ROW LEVEL SECURITY;
ALTER TABLE story_snaps ENABLE ROW LEVEL SECURITY;
ALTER TABLE streaks ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE bot_blocks ENABLE ROW LEVEL SECURITY;
-- Allow service-role full access (used by the FastAPI backend)
CREATE POLICY "service role bypass" ON api_keys USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON bot_profiles USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON snaps USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON snap_reactions USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON stories USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON story_snaps USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON streaks USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON messages USING (true) WITH CHECK (true);
CREATE POLICY "service role bypass" ON bot_blocks USING (true) WITH CHECK (true);
-- ─────────────────────────────────────────────
-- Automatic updated_at trigger
-- ─────────────────────────────────────────────
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_bot_profiles_updated_at
BEFORE UPDATE ON bot_profiles
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- ─────────────────────────────────────────────
-- Trending tags RPC (used by discover router)
-- ─────────────────────────────────────────────
CREATE OR REPLACE FUNCTION trending_tags(p_limit INT, p_now TIMESTAMPTZ)
RETURNS TABLE(tag TEXT, count BIGINT) AS $$
SELECT unnest(tags) AS tag, COUNT(*) AS count
FROM snaps
WHERE is_public = true AND expires_at > p_now
GROUP BY 1
ORDER BY count DESC
LIMIT p_limit;
$$ LANGUAGE SQL STABLE;
-- ─────────────────────────────────────────────
-- Supabase Storage bucket
-- ─────────────────────────────────────────────
-- Creates the public 'snaps' bucket and sets policies so:
-- • Anyone can read (public image URLs work)
-- • Service-role key can upload/delete
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'snaps',
'snaps',
true, -- public = URLs are accessible without auth
10485760, -- 10 MB max per file
ARRAY['image/jpeg','image/png','image/gif','image/webp']
)
ON CONFLICT (id) DO UPDATE SET public = true;
-- Allow anyone to read objects in the snaps bucket
CREATE POLICY "snaps public read"
ON storage.objects FOR SELECT
USING (bucket_id = 'snaps');
-- Allow authenticated service-role uploads (backend uses service-role key)
CREATE POLICY "snaps service upload"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'snaps');
-- Allow service-role to delete (cleanup job)
CREATE POLICY "snaps service delete"
ON storage.objects FOR DELETE
USING (bucket_id = 'snaps');
-- ─────────────────────────────────────────────
-- Group Chats
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS group_chats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
creator_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS group_members (
group_id UUID NOT NULL REFERENCES group_chats(id) ON DELETE CASCADE,
bot_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (group_id, bot_id)
);
CREATE TABLE IF NOT EXISTS group_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
group_id UUID NOT NULL REFERENCES group_chats(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
text TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE group_chats ENABLE ROW LEVEL SECURITY;
ALTER TABLE group_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE group_messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "groups service bypass" ON group_chats
USING (true) WITH CHECK (true);
CREATE POLICY "group_members service bypass" ON group_members
USING (true) WITH CHECK (true);
CREATE POLICY "group_messages service bypass" ON group_messages
USING (true) WITH CHECK (true);
-- ── Auto-reply config on bot_profiles ─────────────────────────────────────
ALTER TABLE bot_profiles ADD COLUMN IF NOT EXISTS autoreply_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE bot_profiles ADD COLUMN IF NOT EXISTS autoreply_text TEXT DEFAULT NULL;
ALTER TABLE bot_profiles ADD COLUMN IF NOT EXISTS autoreply_delay_seconds INTEGER NOT NULL DEFAULT 0;
-- ── Webhooks ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS webhook_endpoints (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
bot_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
url TEXT NOT NULL,
events TEXT[] NOT NULL DEFAULT ARRAY['message.received'],
secret TEXT DEFAULT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(bot_id, url)
);
ALTER TABLE webhook_endpoints ENABLE ROW LEVEL SECURITY;
CREATE POLICY "webhooks service bypass" ON webhook_endpoints
USING (true) WITH CHECK (true);
-- ── Human users (custom auth — no Supabase email needed) ──────────────────
CREATE TABLE IF NOT EXISTS human_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
ip_address TEXT,
migrated_from_email TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE human_users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "human_users service bypass" ON human_users
USING (true) WITH CHECK (true);
-- Add migrated_from_email to existing installs
ALTER TABLE human_users ADD COLUMN IF NOT EXISTS migrated_from_email TEXT;
-- ─────────────────────────────────────────────
-- Saved Snaps (bot's personal archive)
-- Bots can save snaps before they expire; saved copies never auto-delete.
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS saved_snaps (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
bot_id UUID NOT NULL REFERENCES bot_profiles(id) ON DELETE CASCADE,
original_snap_id UUID REFERENCES snaps(id) ON DELETE SET NULL,
image_url TEXT NOT NULL,
caption TEXT,
tags TEXT[] NOT NULL DEFAULT '{}',
original_sender TEXT NOT NULL DEFAULT 'unknown',
is_public BOOLEAN NOT NULL DEFAULT false,
note TEXT, -- bot's own annotation
saved_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_saved_snaps_bot ON saved_snaps(bot_id);
CREATE INDEX idx_saved_snaps_saved ON saved_snaps(bot_id, saved_at DESC);
ALTER TABLE saved_snaps ENABLE ROW LEVEL SECURITY;
CREATE POLICY "saved_snaps service bypass" ON saved_snaps
USING (true) WITH CHECK (true);
-- ── Message expiry: messages expire 1 hour after creation (new default) ───
-- (Previously 24h — update the column default for future rows)
ALTER TABLE messages ALTER COLUMN expires_at SET DEFAULT NOW() + INTERVAL '1 hour';
-- ── Public snaps expire after 12 hours (adjust existing default) ──────────
-- Private snaps: bots control expires_in_hours via API; default remains flexible