- Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathsetup.sql
More file actions
Latest commit
588 lines (523 loc) · 24.1 KB
/
Copy pathsetup.sql
File metadata and controls
588 lines (523 loc) · 24.1 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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
-- =============================================================================
-- SUPABASE DATABASE SETUP - Version 5.0.0
-- =============================================================================
-- Run this SQL in the Supabase SQL Editor to set up all required tables,
-- functions, indexes, and RLS policies for the application.
-- =============================================================================
-- =============================================================================
-- STEP 1: ENABLE REQUIRED EXTENSIONS
-- =============================================================================
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
-- Enable vector extension for document embeddings
-- Note: PostgreSQL does not support indexing vectors with more than 2,000 dimensions
CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA extensions;
-- =============================================================================
-- STEP 2: CREATE USERS TABLE
-- =============================================================================
CREATETABLEIF NOT EXISTS public.users (
id uuid REFERENCESauth.usersNOT NULLPRIMARY KEY,
full_name text,
email text
);
-- Enable Row Level Security
ALTERTABLEpublic.users ENABLE ROW LEVEL SECURITY;
-- RLS Policies for users table
DROP POLICY IF EXISTS "Users can insert own data"ONpublic.users;
CREATE POLICY "Users can insert own data"
ONpublic.users
FOR INSERT
TO public
WITH CHECK (id = (SELECTauth.uid()));
DROP POLICY IF EXISTS "Users can update own data"ONpublic.users;
CREATE POLICY "Users can update own data"
ONpublic.users
FOR UPDATE
TO public
USING (id = (SELECTauth.uid()))
WITH CHECK (id = (SELECTauth.uid()));
DROP POLICY IF EXISTS "Users can view own data"ONpublic.users;
CREATE POLICY "Users can view own data"
ONpublic.users
FOR SELECT
TO public
USING (id = (SELECTauth.uid()));
-- =============================================================================
-- STEP 3: CREATE TRIGGER FOR NEW USER REGISTRATION
-- =============================================================================
-- Trigger function to auto-create user record on signup
CREATE OR REPLACEFUNCTIONpublic.handle_new_user()
RETURNS trigger AS $$
BEGIN
INSERT INTOpublic.users (id, full_name, email)
VALUES (
new.id,
new.raw_user_meta_data->>'full_name',
new.email
);
RETURN new;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger to execute function on new auth user
DROPTRIGGER IF EXISTS on_auth_user_created ONauth.users;
CREATETRIGGERon_auth_user_created
AFTER INSERT ONauth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
-- =============================================================================
-- STEP 4: CREATE CHAT SESSIONS TABLE
-- =============================================================================
CREATETABLEIF NOT EXISTS public.chat_sessions (
id uuid NOT NULL DEFAULT extensions.uuid_generate_v4(),
user_id uuid NOT NULL,
created_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
chat_title textNULL,
-- Per-chat flags used by the chat sidebar menu. A chat can be opened publicly
-- (via /shared-chat/[id]) only while is_public = true.
is_favorite booleanNOT NULL DEFAULT false,
is_public booleanNOT NULL DEFAULT false,
-- Settings the conversation ran with, e.g. { "model": "claude-sonnet-5" }.
-- Written by the chat route on every generation (last-used model wins) and
-- used to restore the model picker when reopening the conversation.
settings jsonb NULL,
CONSTRAINT chat_sessions_pkey PRIMARY KEY (id),
CONSTRAINT chat_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) TABLESPACE pg_default;
-- Indexes for chat_sessions
CREATEINDEXIF NOT EXISTS idx_chat_sessions_user_id
ONpublic.chat_sessions USING btree (user_id) TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS chat_sessions_created_at_idx
ONpublic.chat_sessions USING btree (created_at) TABLESPACE pg_default;
-- Partial indexes keep the "favorites" sidebar group and public lookups cheap
CREATEINDEXIF NOT EXISTS chat_sessions_user_favorite_idx
ONpublic.chat_sessions (user_id, is_favorite)
WHERE is_favorite = true;
CREATEINDEXIF NOT EXISTS chat_sessions_public_idx
ONpublic.chat_sessions (id)
WHERE is_public = true;
-- Enable RLS for chat_sessions
ALTERTABLEpublic.chat_sessions ENABLE ROW LEVEL SECURITY;
-- RLS Policy for chat_sessions
DROP POLICY IF EXISTS "Users can view own chat sessions"ONpublic.chat_sessions;
CREATE POLICY "Users can view own chat sessions"
ONpublic.chat_sessions
AS PERMISSIVE
FOR ALL
TO public
USING (user_id = (SELECTauth.uid()));
-- =============================================================================
-- STEP 5: CREATE MESSAGE PARTS TABLE (Incremental Message Saving)
-- =============================================================================
-- This table stores individual message parts (text, tools, reasoning, etc.)
-- allowing for incremental saving and proper ordering of AI responses
CREATETABLEIF NOT EXISTS public.message_parts (
id uuid NOT NULL DEFAULT gen_random_uuid(),
chat_session_id uuid NOT NULL,
message_id textNOT NULL,
role textNOT NULL,
type textNOT NULL,
"order"integerNOT NULL DEFAULT 0,
created_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Text part fields
text_text textNULL,
text_state textNULL DEFAULT 'done',
-- Reasoning part fields
reasoning_text textNULL,
reasoning_state textNULL DEFAULT 'done',
-- File part fields
file_mediatype textNULL,
file_filename textNULL,
file_url textNULL,
-- Source URL part fields
source_url_id textNULL,
source_url_url textNULL,
source_url_title textNULL,
-- Source Document part fields
source_document_id textNULL,
source_document_mediatype textNULL,
source_document_title textNULL,
source_document_filename textNULL,
-- Tool fields (generic — shared by ALL tools). The `type` column identifies
-- which tool a row belongs to (e.g. 'tool-searchUserDocument')
tool_toolcallid textNULL,
tool_state textNULL,
tool_input jsonb NULL,
tool_output jsonb NULL,
tool_errortext textNULL,
tool_providerexecuted booleanNULL,
tool_approval jsonb NULL,
-- Provider metadata
providermetadata jsonb NULL,
-- Per-step token usage, attached to the first part row saved for each
-- generation step: { model_id, input_tokens, cache_read_tokens,
-- cache_write_tokens, output_tokens }. Aggregated by the /usage and /admin
-- dashboards (WHERE usage IS NOT NULL).
usage jsonb NULL,
-- Constraints
CONSTRAINT message_parts_pkey PRIMARY KEY (id),
CONSTRAINT message_parts_chat_session_id_fkey FOREIGN KEY (chat_session_id)
REFERENCES chat_sessions (id) ON DELETE CASCADE,
CONSTRAINT message_parts_role_check CHECK (
role = ANY (ARRAY['user'::text, 'assistant'::text, 'system'::text])
)
) TABLESPACE pg_default;
-- Indexes for message_parts
CREATEINDEXIF NOT EXISTS idx_message_parts_chat_session_id
ONpublic.message_parts USING btree (chat_session_id) TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS idx_message_parts_message_id
ONpublic.message_parts USING btree (message_id) TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS idx_message_parts_chat_session_message_order
ONpublic.message_parts USING btree (chat_session_id, message_id, "order") TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS idx_message_parts_created_at
ONpublic.message_parts USING btree (created_at) TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS idx_message_parts_type
ONpublic.message_parts USING btree (type) TABLESPACE pg_default;
CREATEINDEXIF NOT EXISTS idx_message_parts_message_order
ONpublic.message_parts USING btree (message_id, "order") TABLESPACE pg_default;
-- Partial index: the usage dashboards only scan rows that carry usage data
CREATEINDEXIF NOT EXISTS idx_message_parts_usage
ONpublic.message_parts USING btree (created_at)
WHERE usage IS NOT NULL;
-- Enable RLS for message_parts
ALTERTABLEpublic.message_parts ENABLE ROW LEVEL SECURITY;
-- RLS Policy for message_parts
DROP POLICY IF EXISTS "Users can view messages from their sessions"ONpublic.message_parts;
CREATE POLICY "Users can view messages from their sessions"
ONpublic.message_parts
AS PERMISSIVE
FOR ALL
TO public
USING (
chat_session_id IN (
SELECTchat_sessions.id
FROM chat_sessions
WHEREchat_sessions.user_id= (SELECTauth.uid())
)
);
-- =============================================================================
-- STEP 6: CREATE USER DOCUMENTS TABLE (Document Metadata)
-- =============================================================================
CREATETABLEIF NOT EXISTS public.user_documents (
id uuid NOT NULL DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
title textNOT NULL,
total_pages integerNOT NULL,
ai_description textNULL,
ai_keyentities text[] NULL,
ai_maintopics text[] NULL,
ai_title textNULL,
file_path textNOT NULL,
created_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp with time zoneNULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT user_documents_pkey PRIMARY KEY (id),
CONSTRAINT user_documents_user_title_unique UNIQUE (user_id, title),
CONSTRAINT user_documents_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) TABLESPACE pg_default;
-- Index for user_documents
CREATEINDEXIF NOT EXISTS idx_user_documents_user_id
ONpublic.user_documents USING btree (user_id) TABLESPACE pg_default;
-- Enable RLS for user_documents
ALTERTABLEpublic.user_documents ENABLE ROW LEVEL SECURITY;
-- RLS Policy for user_documents
DROP POLICY IF EXISTS "Users can only access their own documents"ONpublic.user_documents;
CREATE POLICY "Users can only access their own documents"
ONpublic.user_documents
FOR ALL
TO public
USING ((SELECTauth.uid()) = user_id);
-- =============================================================================
-- STEP 7: CREATE USER DOCUMENTS VECTORS TABLE (Document Embeddings)
-- =============================================================================
CREATETABLEIF NOT EXISTS public.user_documents_vec (
id uuid NOT NULL DEFAULT gen_random_uuid(),
document_id uuid NOT NULL,
text_content textNOT NULL,
page_number integerNOT NULL,
embedding extensions.vector(1024) NULL,
-- Timestamp set automatically when the row is inserted
created_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT user_documents_vec_pkey PRIMARY KEY (id),
CONSTRAINT user_documents_vec_document_page_unique UNIQUE (document_id, page_number),
CONSTRAINT user_documents_vec_document_id_fkey FOREIGN KEY (document_id) REFERENCES user_documents (id) ON DELETE CASCADE
) TABLESPACE pg_default;
-- Index for user_documents_vec
CREATEINDEXIF NOT EXISTS idx_user_documents_vec_document_id
ONpublic.user_documents_vec USING btree (document_id) TABLESPACE pg_default;
-- HNSW index for vector similarity search
-- Parameters: m=16 (connections per layer), ef_construction=200 (build candidate list size)
-- - m=16 is the pgvector default and is fine in almost all cases. Raising it to
-- m=32 roughly DOUBLES the index size for little recall gain on most datasets.
-- - The whole index should fit in Postgres' buffer cache (~25% of the instance RAM)
-- to stay fast. If the index grows larger than that, query latency can degrade.
--
-- NOTE: This index is ONLY used by unfiltered queries (vector ORDER BY + LIMIT,
-- like match_documents below). Adding a WHERE filter on another column disables
-- it -- for that you need a separate partial HNSW index per filter value.
--
-- TODO (only when you grow past ~100k rows of dense text): switch this index to
-- halfvec to roughly halve its size at ~1% recall loss, e.g.
-- USING hnsw ((embedding::halfvec(1024)) halfvec_l2_ops)
-- If you do, also cast to halfvec inside match_documents() so the planner uses it.
-- See the "Tuning the HNSW vector index" section in README.md for details.
CREATEINDEXIF NOT EXISTS user_documents_vec_embedding_idx
ONpublic.user_documents_vec
USING hnsw (embedding extensions.vector_l2_ops)
WITH (m ='16', ef_construction ='200')
TABLESPACE pg_default;
-- Enable RLS for user_documents_vec
ALTERTABLEpublic.user_documents_vec ENABLE ROW LEVEL SECURITY;
-- RLS Policy for user_documents_vec
DROP POLICY IF EXISTS "Users can only access their own document vectors"ONpublic.user_documents_vec;
CREATE POLICY "Users can only access their own document vectors"
ONpublic.user_documents_vec
FOR ALL
TO public
USING (
EXISTS (
SELECT1FROM user_documents
WHEREuser_documents.id=user_documents_vec.document_id
ANDuser_documents.user_id= (SELECTauth.uid())
)
);
-- =============================================================================
-- STEP 8: CREATE HYBRID SEARCH FUNCTION (vector + keyword, RRF-fused)
-- =============================================================================
-- Used by the autonomous document search tool (searchUserDocument). The
-- vector arm catches meaning ("payment terms" matches synonymous phrasing);
-- the keyword arm catches exact tokens (invoice numbers, names, codes) that
-- embeddings blur. Reciprocal Rank Fusion merges the two rankings without
-- score normalization: each result contributes weight / (k + rank) from
-- every list it appears in.
-- Drop the legacy vector-only signature if it exists — leaving it in place
-- creates an overload PostgREST cannot resolve (PGRST202).
DROPFUNCTION IF EXISTS public.match_documents(
vector(1024), int, uuid, uuid[], float
);
CREATE OR REPLACEFUNCTIONpublic.match_documents(
query_embedding vector(1024),
query_text text,
match_count int,
filter_user_id uuid,
file_ids uuid[],
vector_weight float DEFAULT 0.6,
k_rrf int DEFAULT 60
)
RETURNS TABLE (
id uuid,
document_id uuid,
text_content text,
title text,
ai_title text,
ai_description text,
page_number integer,
total_pages integer,
similarity float
)
LANGUAGE sql
AS $$
WITH scoped AS (
-- Filtered candidate set: this user's documents, optionally restricted to
-- specific files. Filtered vector queries can't use the global HNSW index
-- anyway (see the index notes above), so both arms scan this set.
SELECTvec.id, vec.document_id, vec.text_content, vec.page_number,
vec.embedding, doc.title, doc.ai_title, doc.ai_description,
doc.total_pages
FROM user_documents_vec vec
JOIN user_documents doc ONdoc.id=vec.document_id
WHEREdoc.user_id= filter_user_id
ANDdoc.id= ANY(file_ids)
),
vector_hits AS (
SELECTs.id,
ROW_NUMBER() OVER (ORDER BYs.embedding<=> query_embedding) AS rank
FROM scoped s
WHEREs.embeddingIS NOT NULL
ORDER BYs.embedding<=> query_embedding
LIMIT LEAST(GREATEST(match_count, 1) *4, 200)
),
keyword_hits AS (
SELECTs.id,
ROW_NUMBER() OVER (
ORDER BY ts_rank_cd(
to_tsvector('simple', s.text_content),
websearch_to_tsquery('simple', query_text)
) DESC
) AS rank
FROM scoped s
WHERE to_tsvector('simple', s.text_content)
@@ websearch_to_tsquery('simple', query_text)
LIMIT LEAST(GREATEST(match_count, 1) *4, 200)
),
fused AS (
SELECT COALESCE(v.id, k.id) AS id,
COALESCE(vector_weight / (k_rrf +v.rank), 0)
+ COALESCE((1- vector_weight) / (k_rrf +k.rank), 0) AS score
FROM vector_hits v
FULL OUTER JOIN keyword_hits k ONv.id=k.id
)
SELECTs.id, s.document_id, s.text_content, s.title, s.ai_title,
s.ai_description, s.page_number, s.total_pages, f.scoreAS similarity
FROM fused f
JOIN scoped s ONs.id=f.id
ORDER BYf.scoreDESC
LIMIT LEAST(GREATEST(match_count, 1), 50);
$$;
-- =============================================================================
-- STEP 9: AI MODELS CATALOG + PER-USER SELECTION
-- =============================================================================
-- Reference table of the models the app can use. The primary key is an
-- auto-incrementing id; `model_id` is the slug the chat API route switches on
-- and is what users.selected_model points at, so each user's chosen model is
-- visible directly on the users table. Costs are per 1M tokens in USD.
CREATETABLEIF NOT EXISTS public.ai_models (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
model_id textNOT NULL UNIQUE,
display_name textNOT NULL,
provider textNOT NULL,
input_cost_per_million_usd numeric(10, 4) NOT NULL,
output_cost_per_million_usd numeric(10, 4) NOT NULL,
active booleanNOT NULL DEFAULT true,
updated_at timestamp with time zoneNOT NULL DEFAULT now(),
description textNOT NULL DEFAULT '',
source_url textNOT NULL DEFAULT '',
cost_tier textNOT NULL DEFAULT 'medium',
cost_note textNOT NULL DEFAULT '',
display_order integerNOT NULL DEFAULT 0,
selectable booleanNOT NULL DEFAULT true,
CONSTRAINT ai_models_cost_tier_check CHECK (
cost_tier = ANY (ARRAY['low'::text, 'medium'::text, 'high'::text])
)
);
CREATEINDEXIF NOT EXISTS ai_models_display_order_idx
ONpublic.ai_models USING btree (display_order);
-- Reference data: readable by any authenticated user, not writable from the app
ALTERTABLEpublic.ai_models ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Authenticated users can read ai models"ONpublic.ai_models;
CREATE POLICY "Authenticated users can read ai models"
ONpublic.ai_models
FOR SELECT
TO authenticated
USING (true);
-- Seed models. logo_url is intentionally omitted: the UI maps `provider` to a
-- local image in public/images/ai-providers/.
-- The chat API route is Anthropic-only (for prompt caching), so only
-- anthropic-provider models are seeded. All seeded models support adaptive
-- thinking, which the route enables unconditionally. Costs are the official
-- API sticker prices per 1M tokens (they feed the /usage and /admin cost
-- estimates). Note: Sonnet 5 has introductory pricing of $2/$10 per MTok
-- through 2026-08-31 — the sticker price is stored so estimates stay valid
-- after the intro period.
INSERT INTOpublic.ai_models
(model_id, display_name, provider, input_cost_per_million_usd, output_cost_per_million_usd, active, description, source_url, cost_tier, cost_note, display_order, selectable)
VALUES
('claude-sonnet-5', 'Sonnet 5', 'anthropic', 3.0000, 15.0000, true, 'The best combination of speed and intelligence — near-Opus quality on coding and agentic work. Intro pricing ($2/$10 per MTok) until Aug 31, 2026.', 'https://www.anthropic.com/claude', 'medium', '~$0.45/answer', 1, true),
('claude-opus-4-8', 'Opus 4.8', 'anthropic', 5.0000, 25.0000, true, 'Anthropic''s most capable Opus-tier model — complex agentic coding and enterprise work.', 'https://www.anthropic.com/claude', 'high', '~$1.25/answer', 2, true),
('claude-fable-5', 'Fable 5', 'anthropic', 10.0000, 50.0000, true, 'Anthropic''s most capable widely released model — next-generation intelligence for the most demanding reasoning and long-running agents. Requires 30-day data retention on your Anthropic org.', 'https://www.anthropic.com/claude', 'high', '~$2.50/answer', 3, true)
ON CONFLICT (model_id) DO UPDATESET
display_name =EXCLUDED.display_name,
input_cost_per_million_usd =EXCLUDED.input_cost_per_million_usd,
output_cost_per_million_usd =EXCLUDED.output_cost_per_million_usd,
description =EXCLUDED.description,
cost_tier =EXCLUDED.cost_tier,
cost_note =EXCLUDED.cost_note,
display_order =EXCLUDED.display_order,
active = true,
selectable = true;
-- Per-user selected model. Nullable + ON DELETE SET NULL so removing a model
-- doesn't break users; the app falls back to the default when null.
ALTERTABLEpublic.users
ADD COLUMN IF NOT EXISTS selected_model text DEFAULT 'claude-sonnet-5'
REFERENCESpublic.ai_models (model_id) ON DELETESETNULL;
-- =============================================================================
-- STEP 10: STORAGE BUCKET SETUP
-- =============================================================================
-- Note: Create a storage bucket named 'userfiles' in the Supabase dashboard first
-- Then run these policies:
-- Policy 1: Allow users to select their own files
DROP POLICY IF EXISTS "User can select own files"ONstorage.objects;
CREATE POLICY "User can select own files"
ONstorage.objects FOR SELECT
USING (
(bucket_id ='userfiles'::text) AND
((auth.uid())::text= (storage.foldername(name))[1])
);
-- Policy 2: Allow users to insert their own files
DROP POLICY IF EXISTS "User can insert own files"ONstorage.objects;
CREATE POLICY "User can insert own files"
ONstorage.objects FOR INSERT
WITH CHECK (
(bucket_id ='userfiles'::text) AND
((auth.uid())::text= (storage.foldername(name))[1])
);
-- Policy 3: Allow users to update their own files
DROP POLICY IF EXISTS "User can update own files"ONstorage.objects;
CREATE POLICY "User can update own files"
ONstorage.objects FOR UPDATE
USING (
(bucket_id ='userfiles'::text) AND
((auth.uid())::text= (storage.foldername(name))[1])
);
-- Policy 4: Allow users to delete their own files
DROP POLICY IF EXISTS "User can delete own files"ONstorage.objects;
CREATE POLICY "User can delete own files"
ONstorage.objects FOR DELETE
USING (
(bucket_id ='userfiles'::text) AND
((auth.uid())::text= (storage.foldername(name))[1])
);
-- =============================================================================
-- STEP 11: CREATE USER MEMORIES TABLE (saveMemory chat tool)
-- =============================================================================
-- Discrete long-term memories the assistant stores when the user explicitly
-- asks it to remember something ("remember that I prefer short answers").
-- Each memory is one row; the chat API injects all of a user's memories into
-- the system prompt on every request, and the saveMemory tool can list and
-- delete them again ("forget that ...").
CREATETABLEIF NOT EXISTS public.user_memories (
id uuid NOT NULL DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
content textNOT NULL,
created_at timestamp with time zoneNOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT user_memories_pkey PRIMARY KEY (id),
CONSTRAINT user_memories_user_id_fkey FOREIGN KEY (user_id)
REFERENCESpublic.users (id) ON DELETE CASCADE,
-- Keep single memories small; the whole set is injected into every prompt
CONSTRAINT user_memories_content_length CHECK (char_length(content) <=500)
);
CREATEINDEXIF NOT EXISTS idx_user_memories_user_id
ONpublic.user_memories USING btree (user_id);
-- Enable RLS for user_memories
ALTERTABLEpublic.user_memories ENABLE ROW LEVEL SECURITY;
-- RLS Policy for user_memories
DROP POLICY IF EXISTS "Users can manage own memories"ONpublic.user_memories;
CREATE POLICY "Users can manage own memories"
ONpublic.user_memories
FOR ALL
TO public
USING (user_id = (SELECTauth.uid()))
WITH CHECK (user_id = (SELECTauth.uid()));
-- =============================================================================
-- STEP 12: ADMIN FLAG ON USERS
-- =============================================================================
-- Gates the /admin dashboard (user management + org-wide usage overview).
-- Grant yourself access after signing up:
-- UPDATE public.users SET is_admin = true WHERE email = 'you@example.com';
ALTERTABLEpublic.users
ADD COLUMN IF NOT EXISTS is_admin booleanNOT NULL DEFAULT false;
-- =============================================================================
-- SETUP COMPLETE
-- =============================================================================
--
-- After running this SQL:
-- 1. Create a storage bucket named 'userfiles' (set to private)
-- 2. Configure your environment variables in .env.local
-- 3. Set up email templates in Supabase Auth settings
-- 4. (Optional) Grant yourself admin access to the /admin dashboard:
-- UPDATE public.users SET is_admin = true WHERE email = 'you@example.com';
--
-- NOTE: this file is the complete, current schema — and it is idempotent
-- (IF NOT EXISTS / CREATE OR REPLACE throughout), so it is safe to re-run on
-- an existing install to pick up new columns, tables and functions.
--
-- For more information, see the README.md file.
-- =============================================================================