-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsupabase-setup.sql
More file actions
2205 lines (1888 loc) · 81.1 KB
/
supabase-setup.sql
File metadata and controls
2205 lines (1888 loc) · 81.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
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- NextSaaS Complete Database Setup
-- Generated on: 2025-07-07T03:08:04.646Z
--
-- Instructions:
-- 1. Go to your Supabase SQL Editor
-- 2. Create a new query
-- 3. Paste this entire file
-- 4. Click "Run"
--
-- This will create all tables, functions, triggers, and security policies
-- ============================================
-- CORE SCHEMA
-- ============================================
-- ------------------------------------
-- 001_users.sql
-- ------------------------------------
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Core Users Table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
email_verified_at TIMESTAMP WITH TIME ZONE,
name VARCHAR(255),
avatar_url TEXT,
timezone VARCHAR(50) DEFAULT 'UTC',
locale VARCHAR(10) DEFAULT 'en',
metadata JSONB DEFAULT '{}',
last_seen_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Create indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_deleted_at ON users(deleted_at) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_created_at ON users(created_at);
-- Add comments
COMMENT ON TABLE users IS 'Core user table for authentication and user management';
COMMENT ON COLUMN users.id IS 'Unique identifier for the user';
COMMENT ON COLUMN users.email IS 'User email address, must be unique';
COMMENT ON COLUMN users.email_verified_at IS 'Timestamp when email was verified';
COMMENT ON COLUMN users.name IS 'Display name of the user';
COMMENT ON COLUMN users.avatar_url IS 'URL to user avatar image';
COMMENT ON COLUMN users.timezone IS 'User preferred timezone';
COMMENT ON COLUMN users.locale IS 'User preferred locale/language';
COMMENT ON COLUMN users.metadata IS 'Additional user metadata in JSON format';
COMMENT ON COLUMN users.last_seen_at IS 'Last time user was active';
COMMENT ON COLUMN users.deleted_at IS 'Soft delete timestamp';
-- ------------------------------------
-- 002_organizations.sql
-- ------------------------------------
-- Organizations (Tenant Isolation)
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
domain VARCHAR(255),
logo_url TEXT,
settings JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}',
subscription_status VARCHAR(50) DEFAULT 'trial',
trial_ends_at TIMESTAMP WITH TIME ZONE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Create indexes
CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_domain ON organizations(domain) WHERE domain IS NOT NULL;
CREATE INDEX idx_organizations_created_by ON organizations(created_by);
CREATE INDEX idx_organizations_subscription_status ON organizations(subscription_status);
CREATE INDEX idx_organizations_deleted_at ON organizations(deleted_at) WHERE deleted_at IS NULL;
-- Add comments
COMMENT ON TABLE organizations IS 'Organizations provide multi-tenant isolation';
COMMENT ON COLUMN organizations.id IS 'Unique identifier for the organization';
COMMENT ON COLUMN organizations.name IS 'Display name of the organization';
COMMENT ON COLUMN organizations.slug IS 'URL-friendly unique identifier';
COMMENT ON COLUMN organizations.domain IS 'Custom domain for the organization';
COMMENT ON COLUMN organizations.logo_url IS 'URL to organization logo';
COMMENT ON COLUMN organizations.settings IS 'Organization-specific settings';
COMMENT ON COLUMN organizations.metadata IS 'Additional organization metadata';
COMMENT ON COLUMN organizations.subscription_status IS 'Current subscription status (trial, active, cancelled, etc.)';
COMMENT ON COLUMN organizations.trial_ends_at IS 'When the trial period ends';
COMMENT ON COLUMN organizations.created_by IS 'User who created the organization';
-- ------------------------------------
-- 003_memberships.sql
-- ------------------------------------
-- User-Organization Memberships
CREATE TABLE IF NOT EXISTS memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL DEFAULT 'member',
permissions JSONB DEFAULT '[]',
invited_by UUID REFERENCES users(id),
invited_at TIMESTAMP WITH TIME ZONE,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, organization_id)
);
-- Create indexes
CREATE INDEX idx_memberships_user_id ON memberships(user_id);
CREATE INDEX idx_memberships_organization_id ON memberships(organization_id);
CREATE INDEX idx_memberships_role ON memberships(role);
CREATE INDEX idx_memberships_invited_by ON memberships(invited_by);
-- Add comments
COMMENT ON TABLE memberships IS 'Links users to organizations with roles and permissions';
COMMENT ON COLUMN memberships.id IS 'Unique identifier for the membership';
COMMENT ON COLUMN memberships.user_id IS 'User who is a member';
COMMENT ON COLUMN memberships.organization_id IS 'Organization the user belongs to';
COMMENT ON COLUMN memberships.role IS 'Role within the organization (owner, admin, member, etc.)';
COMMENT ON COLUMN memberships.permissions IS 'Additional custom permissions';
COMMENT ON COLUMN memberships.invited_by IS 'User who invited this member';
COMMENT ON COLUMN memberships.invited_at IS 'When the invitation was sent';
COMMENT ON COLUMN memberships.accepted_at IS 'When the invitation was accepted';
-- ------------------------------------
-- 004_organizations_additions.sql
-- ------------------------------------
-- Additional columns for organizations table
-- These are added conditionally to avoid errors if they already exist
DO $$
BEGIN
-- Add website column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'organizations' AND column_name = 'website') THEN
ALTER TABLE organizations ADD COLUMN website VARCHAR(255);
COMMENT ON COLUMN organizations.website IS 'Organization website URL';
END IF;
-- Add description column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'organizations' AND column_name = 'description') THEN
ALTER TABLE organizations ADD COLUMN description TEXT;
COMMENT ON COLUMN organizations.description IS 'Organization description or about text';
END IF;
END $$;
-- ------------------------------------
-- 005_users_profile_additions.sql
-- ------------------------------------
-- Additional profile columns for users table
-- These are added conditionally to avoid errors if they already exist
DO $$
BEGIN
-- Add first_name column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'first_name') THEN
ALTER TABLE users ADD COLUMN first_name VARCHAR(255);
COMMENT ON COLUMN users.first_name IS 'User first name';
END IF;
-- Add last_name column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'last_name') THEN
ALTER TABLE users ADD COLUMN last_name VARCHAR(255);
COMMENT ON COLUMN users.last_name IS 'User last name';
END IF;
-- Add bio column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'bio') THEN
ALTER TABLE users ADD COLUMN bio TEXT;
COMMENT ON COLUMN users.bio IS 'User biography or about text';
END IF;
-- Add phone column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'phone') THEN
ALTER TABLE users ADD COLUMN phone VARCHAR(50);
COMMENT ON COLUMN users.phone IS 'User phone number';
END IF;
-- Add website column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'website') THEN
ALTER TABLE users ADD COLUMN website VARCHAR(255);
COMMENT ON COLUMN users.website IS 'User personal website URL';
END IF;
-- Add current_organization_id column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'current_organization_id') THEN
ALTER TABLE users ADD COLUMN current_organization_id UUID REFERENCES organizations(id);
COMMENT ON COLUMN users.current_organization_id IS 'Currently selected organization for multi-tenant context';
END IF;
END $$;
-- Add indexes for the new columns
CREATE INDEX IF NOT EXISTS idx_users_current_organization_id ON users(current_organization_id);
CREATE INDEX IF NOT EXISTS idx_users_first_name ON users(first_name);
CREATE INDEX IF NOT EXISTS idx_users_last_name ON users(last_name);
-- ------------------------------------
-- 006_memberships_additions.sql
-- ------------------------------------
-- Additional columns for memberships table
-- These are added conditionally to avoid errors if they already exist
DO $$
BEGIN
-- Add status column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'memberships' AND column_name = 'status') THEN
ALTER TABLE memberships ADD COLUMN status VARCHAR(50) DEFAULT 'active' CHECK (status IN ('active', 'invited', 'suspended'));
COMMENT ON COLUMN memberships.status IS 'Membership status (active, invited, suspended)';
END IF;
-- Add joined_at column if not exists (rename accepted_at for clarity)
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'memberships' AND column_name = 'joined_at') THEN
ALTER TABLE memberships ADD COLUMN joined_at TIMESTAMP WITH TIME ZONE;
COMMENT ON COLUMN memberships.joined_at IS 'When the user officially joined the organization';
END IF;
END $$;
-- Add index for status
CREATE INDEX IF NOT EXISTS idx_memberships_status ON memberships(status);
CREATE INDEX IF NOT EXISTS idx_memberships_user_organization_status ON memberships(user_id, organization_id, status);
-- ============================================
-- AUTH SCHEMA
-- ============================================
-- ------------------------------------
-- 001_sessions.sql
-- ------------------------------------
-- User Sessions
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
ip_address INET,
user_agent TEXT,
device_info JSONB DEFAULT '{}',
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
-- Add comments
COMMENT ON TABLE sessions IS 'Active user sessions for authentication';
COMMENT ON COLUMN sessions.id IS 'Unique session identifier';
COMMENT ON COLUMN sessions.user_id IS 'User who owns this session';
COMMENT ON COLUMN sessions.token IS 'Unique session token';
COMMENT ON COLUMN sessions.ip_address IS 'IP address of the session';
COMMENT ON COLUMN sessions.user_agent IS 'Browser user agent string';
COMMENT ON COLUMN sessions.device_info IS 'Additional device information';
COMMENT ON COLUMN sessions.expires_at IS 'When this session expires';
-- ------------------------------------
-- 002_oauth_accounts.sql
-- ------------------------------------
-- OAuth Provider Accounts
CREATE TABLE IF NOT EXISTS oauth_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
access_token TEXT,
refresh_token TEXT,
expires_at TIMESTAMP WITH TIME ZONE,
token_type VARCHAR(50),
scope TEXT,
id_token TEXT,
session_state TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(provider, provider_account_id)
);
-- Create indexes
CREATE INDEX idx_oauth_accounts_user_id ON oauth_accounts(user_id);
CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider);
-- Add comments
COMMENT ON TABLE oauth_accounts IS 'OAuth provider accounts linked to users';
COMMENT ON COLUMN oauth_accounts.provider IS 'OAuth provider name (google, github, etc.)';
COMMENT ON COLUMN oauth_accounts.provider_account_id IS 'Account ID from the OAuth provider';
COMMENT ON COLUMN oauth_accounts.access_token IS 'OAuth access token (encrypted)';
COMMENT ON COLUMN oauth_accounts.refresh_token IS 'OAuth refresh token (encrypted)';
COMMENT ON COLUMN oauth_accounts.expires_at IS 'When the access token expires';
-- ------------------------------------
-- 003_password_resets.sql
-- ------------------------------------
-- Password Reset Tokens
CREATE TABLE IF NOT EXISTS password_resets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_password_resets_user_id ON password_resets(user_id);
CREATE INDEX idx_password_resets_token ON password_resets(token);
CREATE INDEX idx_password_resets_expires_at ON password_resets(expires_at);
-- Add comments
COMMENT ON TABLE password_resets IS 'Password reset tokens for secure password recovery';
COMMENT ON COLUMN password_resets.token IS 'Unique reset token';
COMMENT ON COLUMN password_resets.expires_at IS 'When this token expires';
COMMENT ON COLUMN password_resets.used_at IS 'When this token was used';
-- ------------------------------------
-- 004_email_verifications.sql
-- ------------------------------------
-- Email Verification Tokens
CREATE TABLE IF NOT EXISTS email_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
verified_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_email_verifications_user_id ON email_verifications(user_id);
CREATE INDEX idx_email_verifications_email ON email_verifications(email);
CREATE INDEX idx_email_verifications_token ON email_verifications(token);
CREATE INDEX idx_email_verifications_expires_at ON email_verifications(expires_at);
-- Add comments
COMMENT ON TABLE email_verifications IS 'Email verification tokens';
COMMENT ON COLUMN email_verifications.email IS 'Email address to verify';
COMMENT ON COLUMN email_verifications.token IS 'Unique verification token';
COMMENT ON COLUMN email_verifications.expires_at IS 'When this token expires';
COMMENT ON COLUMN email_verifications.verified_at IS 'When the email was verified';
-- ------------------------------------
-- 005_organization_invitations.sql
-- ------------------------------------
-- Organization Invitations
CREATE TABLE IF NOT EXISTS organization_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('admin', 'member')),
token VARCHAR(255) UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
message TEXT,
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'expired', 'cancelled')),
invited_by UUID NOT NULL REFERENCES users(id),
accepted_by UUID REFERENCES users(id),
expires_at TIMESTAMP WITH TIME ZONE DEFAULT (NOW() + INTERVAL '7 days'),
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_organization_invitations_token ON organization_invitations(token);
CREATE INDEX IF NOT EXISTS idx_organization_invitations_email ON organization_invitations(email);
CREATE INDEX IF NOT EXISTS idx_organization_invitations_organization_id ON organization_invitations(organization_id);
CREATE INDEX IF NOT EXISTS idx_organization_invitations_status ON organization_invitations(status);
CREATE INDEX IF NOT EXISTS idx_organization_invitations_expires_at ON organization_invitations(expires_at);
-- Add comments
COMMENT ON TABLE organization_invitations IS 'Pending invitations to join organizations';
COMMENT ON COLUMN organization_invitations.id IS 'Unique invitation identifier';
COMMENT ON COLUMN organization_invitations.organization_id IS 'Organization being invited to';
COMMENT ON COLUMN organization_invitations.email IS 'Email address of invitee';
COMMENT ON COLUMN organization_invitations.role IS 'Role to be assigned upon acceptance';
COMMENT ON COLUMN organization_invitations.token IS 'Unique token for accepting invitation';
COMMENT ON COLUMN organization_invitations.message IS 'Optional personal message from inviter';
COMMENT ON COLUMN organization_invitations.status IS 'Current status of invitation';
COMMENT ON COLUMN organization_invitations.invited_by IS 'User who sent the invitation';
COMMENT ON COLUMN organization_invitations.accepted_by IS 'User who accepted the invitation';
COMMENT ON COLUMN organization_invitations.expires_at IS 'When this invitation expires';
COMMENT ON COLUMN organization_invitations.accepted_at IS 'When this invitation was accepted';
-- ------------------------------------
-- 006_auth_events.sql
-- ------------------------------------
-- Authentication Events for Security Tracking
CREATE TABLE IF NOT EXISTS auth_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL,
ip_address INET,
user_agent TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_auth_events_user_id ON auth_events(user_id);
CREATE INDEX IF NOT EXISTS idx_auth_events_event_type ON auth_events(event_type);
CREATE INDEX IF NOT EXISTS idx_auth_events_created_at ON auth_events(created_at);
CREATE INDEX IF NOT EXISTS idx_auth_events_user_created ON auth_events(user_id, created_at);
-- Add comments
COMMENT ON TABLE auth_events IS 'Authentication and security events for audit trail';
COMMENT ON COLUMN auth_events.id IS 'Unique event identifier';
COMMENT ON COLUMN auth_events.user_id IS 'User associated with the event (nullable for failed attempts)';
COMMENT ON COLUMN auth_events.event_type IS 'Type of authentication event (login, logout, failed_login, password_reset, etc)';
COMMENT ON COLUMN auth_events.ip_address IS 'IP address where event originated';
COMMENT ON COLUMN auth_events.user_agent IS 'Browser user agent string';
COMMENT ON COLUMN auth_events.metadata IS 'Additional event-specific data';
COMMENT ON COLUMN auth_events.created_at IS 'When the event occurred';
-- ------------------------------------
-- 007_sessions_additions.sql
-- ------------------------------------
-- Additional columns for sessions table
-- These are added conditionally to avoid errors if they already exist
DO $$
BEGIN
-- Add browser column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'browser') THEN
ALTER TABLE sessions ADD COLUMN browser VARCHAR(255);
COMMENT ON COLUMN sessions.browser IS 'Parsed browser name from user agent';
END IF;
-- Add os column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'os') THEN
ALTER TABLE sessions ADD COLUMN os VARCHAR(255);
COMMENT ON COLUMN sessions.os IS 'Parsed operating system from user agent';
END IF;
-- Add location column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'location') THEN
ALTER TABLE sessions ADD COLUMN location VARCHAR(255);
COMMENT ON COLUMN sessions.location IS 'Approximate location based on IP geolocation';
END IF;
-- Add status column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'status') THEN
ALTER TABLE sessions ADD COLUMN status VARCHAR(50) DEFAULT 'active';
COMMENT ON COLUMN sessions.status IS 'Session status (active, revoked, expired)';
END IF;
-- Add last_active_at column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'last_active_at') THEN
ALTER TABLE sessions ADD COLUMN last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
COMMENT ON COLUMN sessions.last_active_at IS 'Last time this session was used';
END IF;
-- Add revoked_at column if not exists
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'sessions' AND column_name = 'revoked_at') THEN
ALTER TABLE sessions ADD COLUMN revoked_at TIMESTAMP WITH TIME ZONE;
COMMENT ON COLUMN sessions.revoked_at IS 'When this session was manually revoked';
END IF;
END $$;
-- Add additional indexes for the new columns
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
CREATE INDEX IF NOT EXISTS idx_sessions_last_active_at ON sessions(last_active_at);
CREATE INDEX IF NOT EXISTS idx_sessions_user_status ON sessions(user_id, status);
-- ============================================
-- BILLING SCHEMA
-- ============================================
-- ------------------------------------
-- 001_plans.sql
-- ------------------------------------
-- Subscription Plans
CREATE TABLE IF NOT EXISTS plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
price_monthly INTEGER, -- in cents
price_yearly INTEGER, -- in cents
currency VARCHAR(3) DEFAULT 'USD',
features JSONB DEFAULT '[]',
limits JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT true,
is_default BOOLEAN DEFAULT false,
stripe_price_id_monthly VARCHAR(255),
stripe_price_id_yearly VARCHAR(255),
stripe_product_id VARCHAR(255),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_plans_slug ON plans(slug);
CREATE INDEX idx_plans_is_active ON plans(is_active);
CREATE INDEX idx_plans_sort_order ON plans(sort_order);
-- Add comments
COMMENT ON TABLE plans IS 'Available subscription plans';
COMMENT ON COLUMN plans.name IS 'Display name of the plan';
COMMENT ON COLUMN plans.slug IS 'URL-friendly identifier';
COMMENT ON COLUMN plans.price_monthly IS 'Monthly price in cents';
COMMENT ON COLUMN plans.price_yearly IS 'Yearly price in cents';
COMMENT ON COLUMN plans.features IS 'Array of feature descriptions';
COMMENT ON COLUMN plans.limits IS 'JSON object with usage limits';
COMMENT ON COLUMN plans.is_default IS 'Whether this is the default plan for new users';
-- ------------------------------------
-- 002_subscriptions.sql
-- ------------------------------------
-- Organization Subscriptions
CREATE TABLE IF NOT EXISTS subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
plan_id UUID REFERENCES plans(id),
stripe_subscription_id VARCHAR(255) UNIQUE,
stripe_customer_id VARCHAR(255),
status VARCHAR(50) NOT NULL DEFAULT 'active',
billing_cycle VARCHAR(20) DEFAULT 'monthly', -- monthly, yearly
current_period_start TIMESTAMP WITH TIME ZONE,
current_period_end TIMESTAMP WITH TIME ZONE,
cancel_at_period_end BOOLEAN DEFAULT false,
canceled_at TIMESTAMP WITH TIME ZONE,
trial_start TIMESTAMP WITH TIME ZONE,
trial_end TIMESTAMP WITH TIME ZONE,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_subscriptions_organization_id ON subscriptions(organization_id);
CREATE INDEX idx_subscriptions_plan_id ON subscriptions(plan_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id);
CREATE INDEX idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id);
-- Add comments
COMMENT ON TABLE subscriptions IS 'Organization subscription records';
COMMENT ON COLUMN subscriptions.organization_id IS 'Organization that owns this subscription';
COMMENT ON COLUMN subscriptions.plan_id IS 'Current subscription plan';
COMMENT ON COLUMN subscriptions.status IS 'Subscription status (active, cancelled, past_due, etc.)';
COMMENT ON COLUMN subscriptions.billing_cycle IS 'Monthly or yearly billing';
COMMENT ON COLUMN subscriptions.cancel_at_period_end IS 'Whether subscription will cancel at period end';
-- ------------------------------------
-- 003_invoices.sql
-- ------------------------------------
-- Invoices
CREATE TABLE IF NOT EXISTS invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
subscription_id UUID REFERENCES subscriptions(id),
stripe_invoice_id VARCHAR(255) UNIQUE,
invoice_number VARCHAR(100) UNIQUE,
status VARCHAR(50) NOT NULL DEFAULT 'draft',
amount_total INTEGER NOT NULL, -- in cents
amount_paid INTEGER DEFAULT 0,
amount_due INTEGER NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
line_items JSONB DEFAULT '[]',
billing_details JSONB DEFAULT '{}',
due_date DATE,
paid_at TIMESTAMP WITH TIME ZONE,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_invoices_organization_id ON invoices(organization_id);
CREATE INDEX idx_invoices_subscription_id ON invoices(subscription_id);
CREATE INDEX idx_invoices_status ON invoices(status);
CREATE INDEX idx_invoices_invoice_number ON invoices(invoice_number);
CREATE INDEX idx_invoices_stripe_invoice_id ON invoices(stripe_invoice_id);
CREATE INDEX idx_invoices_created_at ON invoices(created_at);
-- Add comments
COMMENT ON TABLE invoices IS 'Invoice records for billing';
COMMENT ON COLUMN invoices.amount_total IS 'Total invoice amount in cents';
COMMENT ON COLUMN invoices.status IS 'Invoice status (draft, open, paid, void, uncollectible)';
COMMENT ON COLUMN invoices.line_items IS 'Array of invoice line items';
COMMENT ON COLUMN invoices.billing_details IS 'Customer billing information';
-- ------------------------------------
-- 004_payments.sql
-- ------------------------------------
-- Payment History
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
invoice_id UUID REFERENCES invoices(id),
stripe_payment_intent_id VARCHAR(255) UNIQUE,
stripe_charge_id VARCHAR(255) UNIQUE,
amount INTEGER NOT NULL, -- in cents
currency VARCHAR(3) DEFAULT 'USD',
status VARCHAR(50) NOT NULL DEFAULT 'pending',
payment_method VARCHAR(50),
payment_method_details JSONB DEFAULT '{}',
failure_reason TEXT,
refunded_amount INTEGER DEFAULT 0,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_payments_organization_id ON payments(organization_id);
CREATE INDEX idx_payments_invoice_id ON payments(invoice_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_stripe_payment_intent_id ON payments(stripe_payment_intent_id);
CREATE INDEX idx_payments_created_at ON payments(created_at);
-- Add comments
COMMENT ON TABLE payments IS 'Payment transaction records';
COMMENT ON COLUMN payments.amount IS 'Payment amount in cents';
COMMENT ON COLUMN payments.status IS 'Payment status (pending, succeeded, failed, refunded)';
COMMENT ON COLUMN payments.payment_method IS 'Payment method type (card, bank_transfer, etc.)';
COMMENT ON COLUMN payments.refunded_amount IS 'Amount refunded in cents';
-- ------------------------------------
-- 005_usage_tracking.sql
-- ------------------------------------
-- Usage Tracking
CREATE TABLE IF NOT EXISTS usage_tracking (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
feature VARCHAR(100) NOT NULL,
usage_count INTEGER DEFAULT 0,
usage_limit INTEGER,
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
period_end TIMESTAMP WITH TIME ZONE NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, feature, period_start)
);
-- Create indexes
CREATE INDEX idx_usage_tracking_organization_id ON usage_tracking(organization_id);
CREATE INDEX idx_usage_tracking_feature ON usage_tracking(feature);
CREATE INDEX idx_usage_tracking_period ON usage_tracking(period_start, period_end);
-- Add comments
COMMENT ON TABLE usage_tracking IS 'Track feature usage for billing and limits';
COMMENT ON COLUMN usage_tracking.feature IS 'Feature identifier (api_calls, storage_gb, etc.)';
COMMENT ON COLUMN usage_tracking.usage_count IS 'Current usage count';
COMMENT ON COLUMN usage_tracking.usage_limit IS 'Usage limit for this period';
COMMENT ON COLUMN usage_tracking.period_start IS 'Start of the billing period';
COMMENT ON COLUMN usage_tracking.period_end IS 'End of the billing period';
-- ============================================
-- CONTENT SCHEMA
-- ============================================
-- ------------------------------------
-- 001_projects.sql
-- ------------------------------------
-- Generic Projects/Workspaces
CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100),
description TEXT,
type VARCHAR(100) DEFAULT 'general', -- real_estate, crypto, ecommerce, etc.
settings JSONB DEFAULT '{}',
metadata JSONB DEFAULT '{}',
is_archived BOOLEAN DEFAULT false,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
UNIQUE(organization_id, slug)
);
-- Create indexes
CREATE INDEX idx_projects_organization_id ON projects(organization_id);
CREATE INDEX idx_projects_type ON projects(type);
CREATE INDEX idx_projects_created_by ON projects(created_by);
CREATE INDEX idx_projects_is_archived ON projects(is_archived);
CREATE INDEX idx_projects_deleted_at ON projects(deleted_at) WHERE deleted_at IS NULL;
-- Add comments
COMMENT ON TABLE projects IS 'Generic projects/workspaces for organizing content';
COMMENT ON COLUMN projects.type IS 'Project type for industry-specific features';
COMMENT ON COLUMN projects.slug IS 'URL-friendly identifier within organization';
COMMENT ON COLUMN projects.settings IS 'Project-specific settings';
COMMENT ON COLUMN projects.is_archived IS 'Whether project is archived';
-- ------------------------------------
-- 002_items.sql
-- ------------------------------------
-- Flexible Content Items
CREATE TABLE IF NOT EXISTS items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
parent_id UUID REFERENCES items(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL, -- property, crypto_position, product, task, etc.
title VARCHAR(255) NOT NULL,
slug VARCHAR(255),
description TEXT,
content TEXT, -- Rich text content
data JSONB DEFAULT '{}', -- Flexible data storage
metadata JSONB DEFAULT '{}',
status VARCHAR(50) DEFAULT 'active',
priority INTEGER DEFAULT 0,
position INTEGER DEFAULT 0, -- For ordering
tags TEXT[] DEFAULT '{}',
created_by UUID NOT NULL REFERENCES users(id),
assigned_to UUID REFERENCES users(id),
due_date TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE,
UNIQUE(organization_id, project_id, slug)
);
-- Create indexes
CREATE INDEX idx_items_organization_id ON items(organization_id);
CREATE INDEX idx_items_project_id ON items(project_id);
CREATE INDEX idx_items_parent_id ON items(parent_id);
CREATE INDEX idx_items_type ON items(type);
CREATE INDEX idx_items_status ON items(status);
CREATE INDEX idx_items_created_by ON items(created_by);
CREATE INDEX idx_items_assigned_to ON items(assigned_to);
CREATE INDEX idx_items_tags ON items USING GIN(tags);
CREATE INDEX idx_items_deleted_at ON items(deleted_at) WHERE deleted_at IS NULL;
CREATE INDEX idx_items_data ON items USING GIN(data);
-- Add comments
COMMENT ON TABLE items IS 'Flexible content items that can represent any entity type';
COMMENT ON COLUMN items.type IS 'Item type (property, product, task, etc.)';
COMMENT ON COLUMN items.parent_id IS 'Parent item for hierarchical structures';
COMMENT ON COLUMN items.data IS 'Flexible JSON data based on item type';
COMMENT ON COLUMN items.position IS 'Order position within parent or project';
COMMENT ON COLUMN items.tags IS 'Array of tags for categorization';
-- ------------------------------------
-- 003_categories.sql
-- ------------------------------------
-- Categories for Content Organization
CREATE TABLE IF NOT EXISTS categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
parent_id UUID REFERENCES categories(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
description TEXT,
icon VARCHAR(50),
color VARCHAR(7), -- Hex color
metadata JSONB DEFAULT '{}',
position INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, parent_id, slug)
);
-- Create indexes
CREATE INDEX idx_categories_organization_id ON categories(organization_id);
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
CREATE INDEX idx_categories_slug ON categories(slug);
-- Item-Category relationship
CREATE TABLE IF NOT EXISTS item_categories (
item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (item_id, category_id)
);
-- Create indexes
CREATE INDEX idx_item_categories_item_id ON item_categories(item_id);
CREATE INDEX idx_item_categories_category_id ON item_categories(category_id);
-- Add comments
COMMENT ON TABLE categories IS 'Hierarchical categories for organizing content';
COMMENT ON COLUMN categories.parent_id IS 'Parent category for nested structures';
COMMENT ON TABLE item_categories IS 'Many-to-many relationship between items and categories';
-- ------------------------------------
-- 004_attachments.sql
-- ------------------------------------
-- File Attachments
CREATE TABLE IF NOT EXISTS attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
entity_type VARCHAR(50) NOT NULL, -- item, project, user, etc.
entity_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_size BIGINT NOT NULL,
mime_type VARCHAR(100),
storage_provider VARCHAR(50) DEFAULT 'supabase', -- supabase, s3, etc.
metadata JSONB DEFAULT '{}',
uploaded_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_attachments_organization_id ON attachments(organization_id);
CREATE INDEX idx_attachments_entity ON attachments(entity_type, entity_id);
CREATE INDEX idx_attachments_uploaded_by ON attachments(uploaded_by);
-- Add comments
COMMENT ON TABLE attachments IS 'File attachments for various entities';
COMMENT ON COLUMN attachments.entity_type IS 'Type of entity this attachment belongs to';
COMMENT ON COLUMN attachments.entity_id IS 'ID of the entity this attachment belongs to';
COMMENT ON COLUMN attachments.file_size IS 'File size in bytes';
COMMENT ON COLUMN attachments.storage_provider IS 'Storage provider where file is stored';
-- ------------------------------------
-- 005_custom_fields.sql
-- ------------------------------------
-- Dynamic Custom Fields
CREATE TABLE IF NOT EXISTS custom_fields (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
entity_type VARCHAR(100) NOT NULL, -- items, projects, users, etc.
field_name VARCHAR(100) NOT NULL,
field_label VARCHAR(255) NOT NULL,
field_type VARCHAR(50) NOT NULL, -- text, number, date, select, multiselect, boolean, json
field_options JSONB DEFAULT '{}', -- For select/multiselect options
validation_rules JSONB DEFAULT '{}', -- Validation constraints
default_value JSONB,
is_required BOOLEAN DEFAULT false,
is_searchable BOOLEAN DEFAULT true,
is_visible BOOLEAN DEFAULT true,
help_text TEXT,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(organization_id, entity_type, field_name)
);
-- Create indexes
CREATE INDEX idx_custom_fields_organization_id ON custom_fields(organization_id);
CREATE INDEX idx_custom_fields_entity_type ON custom_fields(entity_type);
CREATE INDEX idx_custom_fields_sort_order ON custom_fields(sort_order);
-- Custom Field Values
CREATE TABLE IF NOT EXISTS custom_field_values (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
custom_field_id UUID NOT NULL REFERENCES custom_fields(id) ON DELETE CASCADE,
entity_id UUID NOT NULL,
value JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(custom_field_id, entity_id)
);
-- Create indexes
CREATE INDEX idx_custom_field_values_custom_field_id ON custom_field_values(custom_field_id);
CREATE INDEX idx_custom_field_values_entity_id ON custom_field_values(entity_id);
CREATE INDEX idx_custom_field_values_value ON custom_field_values USING GIN(value);
-- Add comments
COMMENT ON TABLE custom_fields IS 'Dynamic custom fields for extending entity schemas';
COMMENT ON COLUMN custom_fields.entity_type IS 'Type of entity this field applies to';
COMMENT ON COLUMN custom_fields.field_type IS 'Data type of the custom field';
COMMENT ON COLUMN custom_fields.field_options IS 'Options for select fields';
COMMENT ON COLUMN custom_fields.validation_rules IS 'JSON schema for field validation';
COMMENT ON TABLE custom_field_values IS 'Values for custom fields';
-- ============================================
-- SYSTEM SCHEMA
-- ============================================
-- ------------------------------------
-- 001_audit_logs.sql
-- ------------------------------------
-- Comprehensive Audit Logging
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
table_name VARCHAR(100) NOT NULL,
record_id UUID NOT NULL,
action VARCHAR(20) NOT NULL, -- INSERT, UPDATE, DELETE, ACCESS
old_data JSONB,
new_data JSONB,
changed_fields TEXT[],
ip_address INET,
user_agent TEXT,
session_id UUID,
request_id VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Partition by month for better performance
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX idx_audit_logs_organization_id ON audit_logs(organization_id);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_table_record ON audit_logs(table_name, record_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
-- Add comments
COMMENT ON TABLE audit_logs IS 'Comprehensive audit trail for all data changes';
COMMENT ON COLUMN audit_logs.action IS 'Type of action performed';
COMMENT ON COLUMN audit_logs.old_data IS 'Data before the change (for UPDATE/DELETE)';
COMMENT ON COLUMN audit_logs.new_data IS 'Data after the change (for INSERT/UPDATE)';
COMMENT ON COLUMN audit_logs.changed_fields IS 'Array of field names that changed';
-- ------------------------------------
-- 002_activities.sql
-- ------------------------------------
-- Activity Feed
CREATE TABLE IF NOT EXISTS activities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
action VARCHAR(100) NOT NULL, -- created_item, updated_project, invited_user, etc.
entity_type VARCHAR(100),
entity_id UUID,
entity_title VARCHAR(255),
description TEXT,
metadata JSONB DEFAULT '{}',
is_public BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_activities_organization_id ON activities(organization_id);
CREATE INDEX idx_activities_project_id ON activities(project_id);
CREATE INDEX idx_activities_user_id ON activities(user_id);
CREATE INDEX idx_activities_entity ON activities(entity_type, entity_id);
CREATE INDEX idx_activities_created_at ON activities(created_at DESC);
CREATE INDEX idx_activities_action ON activities(action);
-- Add comments
COMMENT ON TABLE activities IS 'User activity feed for organization';
COMMENT ON COLUMN activities.action IS 'Type of action performed';
COMMENT ON COLUMN activities.entity_type IS 'Type of entity affected';
COMMENT ON COLUMN activities.entity_id IS 'ID of entity affected';
COMMENT ON COLUMN activities.is_public IS 'Whether activity is visible to all org members';
-- ------------------------------------
-- 003_notifications.sql
-- ------------------------------------