-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsupabase-schema.sql
More file actions
472 lines (387 loc) · 18.2 KB
/
Copy pathsupabase-schema.sql
File metadata and controls
472 lines (387 loc) · 18.2 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
-- ============================================
-- Supabase Database Schema for Refleva
-- ============================================
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- 1. USER PROFILES
-- ============================================
-- Extends Supabase auth.users with additional profile information
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
-- Enable Row Level Security
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Policy: Users can read their own profile
CREATE POLICY "Users can view own profile" ON public.profiles
FOR SELECT USING (auth.uid() = id);
-- Policy: Users can update their own profile
CREATE POLICY "Users can update own profile" ON public.profiles
FOR UPDATE USING (auth.uid() = id);
-- Policy: Users can create their own profile (fallback if trigger fails)
CREATE POLICY "Users can create own profile" ON public.profiles
FOR INSERT WITH CHECK (auth.uid() = id);
-- ============================================
-- 2. JOURNAL ENTRIES
-- ============================================
CREATE TABLE IF NOT EXISTS public.journal_entries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
entry_type TEXT NOT NULL CHECK (entry_type IN ('event', 'reflection', 'core')),
title TEXT,
content TEXT NOT NULL,
date_created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
-- Index for faster queries
CREATE INDEX IF NOT EXISTS idx_journal_entries_user_id ON public.journal_entries(user_id);
CREATE INDEX IF NOT EXISTS idx_journal_entries_date_created ON public.journal_entries(date_created);
CREATE INDEX IF NOT EXISTS idx_journal_entries_entry_type ON public.journal_entries(entry_type);
-- Enable Row Level Security
ALTER TABLE public.journal_entries ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only access their own journal entries
CREATE POLICY "Users can view own journal entries" ON public.journal_entries
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can create own journal entries" ON public.journal_entries
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own journal entries" ON public.journal_entries
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own journal entries" ON public.journal_entries
FOR DELETE USING (auth.uid() = user_id);
-- ============================================
-- 3. JOURNAL TAGS
-- ============================================
CREATE TABLE IF NOT EXISTS public.journal_tags (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
UNIQUE(user_id, name)
);
CREATE INDEX IF NOT EXISTS idx_journal_tags_user_id ON public.journal_tags(user_id);
ALTER TABLE public.journal_tags ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own tags" ON public.journal_tags
FOR ALL USING (auth.uid() = user_id);
-- ============================================
-- 4. JOURNAL ENTRY TAGS (Many-to-Many)
-- ============================================
CREATE TABLE IF NOT EXISTS public.journal_entry_tags (
entry_id UUID NOT NULL REFERENCES public.journal_entries(id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES public.journal_tags(id) ON DELETE CASCADE,
PRIMARY KEY (entry_id, tag_id)
);
CREATE INDEX IF NOT EXISTS idx_journal_entry_tags_entry_id ON public.journal_entry_tags(entry_id);
CREATE INDEX IF NOT EXISTS idx_journal_entry_tags_tag_id ON public.journal_entry_tags(tag_id);
ALTER TABLE public.journal_entry_tags ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage entry tags for own entries" ON public.journal_entry_tags
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.journal_entries
WHERE journal_entries.id = journal_entry_tags.entry_id
AND journal_entries.user_id = auth.uid()
)
);
-- ============================================
-- 5. HABITS (Long-term Goals)
-- ============================================
CREATE TABLE IF NOT EXISTS public.habits (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
is_active BOOLEAN DEFAULT TRUE NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_habits_user_id ON public.habits(user_id);
ALTER TABLE public.habits ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own habits" ON public.habits
FOR ALL USING (auth.uid() = user_id);
-- ============================================
-- 6. DAILY TASKS
-- ============================================
CREATE TABLE IF NOT EXISTS public.daily_tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
habit_id UUID NOT NULL REFERENCES public.habits(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL,
task_date DATE NOT NULL,
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
UNIQUE(habit_id, task_date)
);
CREATE INDEX IF NOT EXISTS idx_daily_tasks_user_id ON public.daily_tasks(user_id);
CREATE INDEX IF NOT EXISTS idx_daily_tasks_habit_id ON public.daily_tasks(habit_id);
CREATE INDEX IF NOT EXISTS idx_daily_tasks_task_date ON public.daily_tasks(task_date);
ALTER TABLE public.daily_tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own tasks" ON public.daily_tasks
FOR ALL USING (auth.uid() = user_id);
-- ============================================
-- 7. TASK HISTORY (for visualization)
-- ============================================
-- This table stores historical task completion data
-- Can be populated from daily_tasks or maintained separately
CREATE TABLE IF NOT EXISTS public.task_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
habit_id UUID NOT NULL REFERENCES public.habits(id) ON DELETE CASCADE,
task_date DATE NOT NULL,
is_completed BOOLEAN NOT NULL,
completed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
UNIQUE(user_id, habit_id, task_date)
);
CREATE INDEX IF NOT EXISTS idx_task_history_user_id ON public.task_history(user_id);
CREATE INDEX IF NOT EXISTS idx_task_history_habit_id ON public.task_history(habit_id);
CREATE INDEX IF NOT EXISTS idx_task_history_task_date ON public.task_history(task_date);
ALTER TABLE public.task_history ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own task history" ON public.task_history
FOR SELECT USING (auth.uid() = user_id);
-- ============================================
-- 8. INSIGHTS
-- ============================================
CREATE TABLE IF NOT EXISTS public.insights (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT NOT NULL,
insight_type TEXT NOT NULL CHECK (insight_type IN ('journal', 'habits', 'combined')),
generated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_insights_user_id ON public.insights(user_id);
CREATE INDEX IF NOT EXISTS idx_insights_generated_at ON public.insights(generated_at);
ALTER TABLE public.insights ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own insights" ON public.insights
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can create own insights" ON public.insights
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- ============================================
-- 9. USER PREFERENCES/SETTINGS
-- ============================================
CREATE TABLE IF NOT EXISTS public.user_preferences (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL UNIQUE REFERENCES public.profiles(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON public.user_preferences(user_id);
ALTER TABLE public.user_preferences ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own preferences" ON public.user_preferences
FOR ALL USING (auth.uid() = user_id);
-- Allow users to create their own preferences row
CREATE POLICY "Users can create own preferences" ON public.user_preferences
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- ============================================
-- 10. THERAPISTS
-- ============================================
CREATE TABLE IF NOT EXISTS public.therapists (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT NOT NULL,
license_number TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_therapists_user_id ON public.therapists(user_id);
ALTER TABLE public.therapists ENABLE ROW LEVEL SECURITY;
-- Therapists can view their own profile
CREATE POLICY "Therapists can view own profile" ON public.therapists
FOR SELECT USING (auth.uid() = user_id);
-- ============================================
-- 11. USER-THERAPIST CONNECTIONS
-- ============================================
CREATE TABLE IF NOT EXISTS public.user_therapist_connections (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
therapist_id UUID NOT NULL REFERENCES public.therapists(id) ON DELETE CASCADE,
-- Sharing settings: what the user wants to share with this therapist
share_journal BOOLEAN DEFAULT FALSE NOT NULL,
share_habits BOOLEAN DEFAULT FALSE NOT NULL,
share_insights BOOLEAN DEFAULT FALSE NOT NULL,
connected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
UNIQUE(user_id, therapist_id)
);
CREATE INDEX IF NOT EXISTS idx_user_therapist_connections_user_id ON public.user_therapist_connections(user_id);
CREATE INDEX IF NOT EXISTS idx_user_therapist_connections_therapist_id ON public.user_therapist_connections(therapist_id);
ALTER TABLE public.user_therapist_connections ENABLE ROW LEVEL SECURITY;
-- Users can view and manage their own connections
CREATE POLICY "Users can manage own therapist connections" ON public.user_therapist_connections
FOR ALL USING (auth.uid() = user_id);
-- Therapists can view connections where they are the therapist
CREATE POLICY "Therapists can view connections" ON public.user_therapist_connections
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.therapists
WHERE therapists.id = user_therapist_connections.therapist_id
AND therapists.user_id = auth.uid()
)
);
-- ============================================
-- 12. PROFILE STATS
-- ============================================
CREATE TABLE IF NOT EXISTS public.profile_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL UNIQUE REFERENCES public.profiles(id) ON DELETE CASCADE,
-- Computed stats (can be calculated or maintained)
total_events INTEGER DEFAULT 0 NOT NULL,
total_reflections INTEGER DEFAULT 0 NOT NULL,
total_core INTEGER DEFAULT 0 NOT NULL,
total_tasks_completed INTEGER DEFAULT 0 NOT NULL,
current_streak INTEGER DEFAULT 0 NOT NULL,
longest_streak INTEGER DEFAULT 0 NOT NULL,
last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_profile_stats_user_id ON public.profile_stats(user_id);
ALTER TABLE public.profile_stats ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own stats" ON public.profile_stats
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can update own stats" ON public.profile_stats
FOR UPDATE USING (auth.uid() = user_id);
-- Allow users to create their own stats row
CREATE POLICY "Users can create own stats" ON public.profile_stats
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- ============================================
-- 13. BADGES
-- ============================================
CREATE TABLE IF NOT EXISTS public.badges (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL,
icon TEXT,
criteria TEXT NOT NULL, -- JSON or text describing how to earn this badge
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
-- ============================================
-- 14. USER BADGES (Many-to-Many)
-- ============================================
CREATE TABLE IF NOT EXISTS public.user_badges (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
badge_id UUID NOT NULL REFERENCES public.badges(id) ON DELETE CASCADE,
earned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
UNIQUE(user_id, badge_id)
);
CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON public.user_badges(user_id);
CREATE INDEX IF NOT EXISTS idx_user_badges_badge_id ON public.user_badges(badge_id);
ALTER TABLE public.user_badges ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own badges" ON public.user_badges
FOR SELECT USING (auth.uid() = user_id);
-- ============================================
-- FUNCTIONS & TRIGGERS
-- ============================================
-- Function to automatically create profile when user signs up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, name, email)
VALUES (
NEW.id,
COALESCE(NEW.raw_user_meta_data->>'name', NEW.email),
NEW.email
);
-- Create default preferences
INSERT INTO public.user_preferences (user_id)
VALUES (NEW.id);
-- Initialize profile stats
INSERT INTO public.profile_stats (user_id)
VALUES (NEW.id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger to run the function when a new user is created
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Add updated_at triggers to relevant tables
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON public.profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_journal_entries_updated_at
BEFORE UPDATE ON public.journal_entries
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_habits_updated_at
BEFORE UPDATE ON public.habits
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_user_preferences_updated_at
BEFORE UPDATE ON public.user_preferences
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER update_user_therapist_connections_updated_at
BEFORE UPDATE ON public.user_therapist_connections
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-- Function to update profile stats when journal entry is created
CREATE OR REPLACE FUNCTION public.update_journal_stats()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.entry_type = 'event' THEN
UPDATE public.profile_stats
SET total_events = total_events + 1,
last_updated = NOW()
WHERE user_id = NEW.user_id;
ELSIF NEW.entry_type = 'reflection' THEN
UPDATE public.profile_stats
SET total_reflections = total_reflections + 1,
last_updated = NOW()
WHERE user_id = NEW.user_id;
ELSIF NEW.entry_type = 'core' THEN
UPDATE public.profile_stats
SET total_core = total_core + 1,
last_updated = NOW()
WHERE user_id = NEW.user_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER update_stats_on_journal_entry
AFTER INSERT ON public.journal_entries
FOR EACH ROW EXECUTE FUNCTION public.update_journal_stats();
-- Function to update task completion stats
CREATE OR REPLACE FUNCTION public.update_task_stats()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.is_completed = TRUE AND (OLD.is_completed IS NULL OR OLD.is_completed = FALSE) THEN
UPDATE public.profile_stats
SET total_tasks_completed = total_tasks_completed + 1,
last_updated = NOW()
WHERE user_id = NEW.user_id;
-- Also insert into task_history
INSERT INTO public.task_history (user_id, habit_id, task_date, is_completed, completed_at)
VALUES (NEW.user_id, NEW.habit_id, NEW.task_date, TRUE, NEW.completed_at)
ON CONFLICT (user_id, habit_id, task_date)
DO UPDATE SET is_completed = TRUE, completed_at = NEW.completed_at;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER update_stats_on_task_completion
AFTER INSERT OR UPDATE ON public.daily_tasks
FOR EACH ROW EXECUTE FUNCTION public.update_task_stats();
-- ============================================
-- INITIAL BADGES (Optional - seed data)
-- ============================================
INSERT INTO public.badges (name, description, criteria) VALUES
('First Steps', 'Created your first journal entry', '{"type": "journal_entry", "count": 1}'),
('Reflector', 'Created 10 reflections', '{"type": "reflection", "count": 10}'),
('Event Keeper', 'Created 10 events', '{"type": "event", "count": 10}'),
('Core Moments', 'Created 5 core memories', '{"type": "core", "count": 5}'),
('Consistent', 'Completed tasks for 7 days straight', '{"type": "streak", "days": 7}'),
('Dedicated', 'Completed tasks for 30 days straight', '{"type": "streak", "days": 30}'),
('Task Master', 'Completed 100 tasks', '{"type": "tasks_completed", "count": 100}')
ON CONFLICT (name) DO NOTHING;