-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathschema.sql
More file actions
643 lines (604 loc) · 28.1 KB
/
Copy pathschema.sql
File metadata and controls
643 lines (604 loc) · 28.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
-- GOVbot Supabase Schema
-- Run this in the Supabase SQL Editor to recreate all tables from scratch.
-- Generated 2026-05-11 from code inspection.
--
-- OTP fix note:
-- If /auth/send-otp logs PGRST205 or says otp_rate_limits / otp_codes is
-- missing, run the otp_codes and otp_rate_limits CREATE TABLE blocks below in
-- the Supabase SQL Editor, then restart the backend so PostgREST refreshes its
-- schema cache.
-- ------------------------------------------------------------
-- 1. sessions — per-phone FSM state
-- ------------------------------------------------------------
create table if not exists sessions (
phone text primary key,
state text not null default 'greeting',
collected_data jsonb not null default '{}',
updated_at timestamptz not null default now()
);
-- ------------------------------------------------------------
-- 2. applications — submitted scholarship applications
-- ------------------------------------------------------------
create table if not exists applications (
id uuid primary key default gen_random_uuid(),
phone text not null,
confirmation_number text not null unique,
service text not null,
status text not null default 'submitted',
portal text not null default 'nsp',
timeline_steps jsonb not null default '[]',
submitted_at timestamptz not null default now()
);
create index if not exists applications_phone_idx on applications(phone);
create index if not exists applications_phone_portal_submitted_idx
on applications(phone, portal, submitted_at desc);
-- ------------------------------------------------------------
-- 3. otp_codes — one-time passwords (stored hashed)
-- ------------------------------------------------------------
create table if not exists otp_codes (
id uuid primary key default gen_random_uuid(),
phone text not null,
purpose text not null default 'login'
check (purpose in ('login', 'digilocker', 'bank_verify')),
code text not null, -- SHA-256 hex digest
expires_at timestamptz not null,
used boolean not null default false
);
create index if not exists otp_codes_phone_idx on otp_codes(phone);
alter table if exists otp_codes
add column if not exists purpose text not null default 'login';
do $$
begin
if not exists (
select 1
from pg_constraint
where conname = 'otp_codes_purpose_check'
and conrelid = 'otp_codes'::regclass
) then
alter table otp_codes
add constraint otp_codes_purpose_check
check (purpose in ('login', 'digilocker', 'bank_verify'));
end if;
end $$;
create index if not exists otp_codes_phone_purpose_idx on otp_codes(phone, purpose);
create index if not exists otp_codes_active_lookup_idx
on otp_codes(phone, purpose, code, expires_at)
where used = false;
-- ------------------------------------------------------------
-- 4. eligibility_checks — screener audit log
-- ------------------------------------------------------------
create table if not exists eligibility_checks (
id uuid primary key default gen_random_uuid(),
income integer not null,
caste text not null,
course_level text not null,
marks_pct numeric(5,2) not null,
eligible boolean not null,
schemes text[] not null default '{}',
reasons text[] not null default '{}',
created_at timestamptz not null default now()
);
-- ------------------------------------------------------------
-- 5. ocr_extractions — Aadhaar OCR results
-- NOTE: field_map stores extracted fields; aadhaar_number
-- is masked to last-4 digits before insert (PII mitigation).
-- ------------------------------------------------------------
create table if not exists ocr_extractions (
id uuid primary key default gen_random_uuid(),
session_id text,
phone text,
raw_text text,
field_map jsonb not null default '{}',
confidence numeric(4,3) not null default 0
);
create index if not exists ocr_extractions_phone_idx on ocr_extractions(phone);
-- ------------------------------------------------------------
-- 6. document_checks — doc validity results
-- ------------------------------------------------------------
create table if not exists document_checks (
id uuid primary key default gen_random_uuid(),
session_id text,
phone text,
doc_type text not null,
issue_date date,
expiry_date date,
valid boolean not null default false,
flags text[] not null default '{}'
);
create index if not exists document_checks_phone_idx on document_checks(phone);
-- ------------------------------------------------------------
-- 7. user_documents — unified KYC vault
-- ------------------------------------------------------------
create table if not exists user_documents (
id uuid primary key default gen_random_uuid(),
phone text not null,
doc_type text not null,
custom_label text,
source text not null,
storage_path text not null,
mime_type text,
original_filename text,
status text not null default 'processing',
verification_status text not null default 'unknown',
issue_date date,
expiry_date date,
ocr_extracted_data jsonb not null default '{}',
user_corrected_data jsonb not null default '{}',
extracted_data jsonb not null default '{}',
source_confidence numeric(4,3) not null default 0,
confidence numeric(4,3) not null default 0,
status_reason text,
edited_by_user boolean not null default false,
edited_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table if exists user_documents add column if not exists ocr_extracted_data jsonb not null default '{}';
alter table if exists user_documents add column if not exists user_corrected_data jsonb not null default '{}';
alter table if exists user_documents add column if not exists source_confidence numeric(4,3) not null default 0;
alter table if exists user_documents add column if not exists status_reason text;
alter table if exists user_documents add column if not exists edited_by_user boolean not null default false;
alter table if exists user_documents add column if not exists edited_at timestamptz;
alter table if exists user_documents add column if not exists custom_label text;
update user_documents
set
ocr_extracted_data = case
when coalesce(ocr_extracted_data, '{}'::jsonb) = '{}'::jsonb then coalesce(extracted_data, '{}'::jsonb)
else ocr_extracted_data
end,
extracted_data = coalesce(extracted_data, '{}'::jsonb),
source_confidence = case
when coalesce(source_confidence, 0) = 0 then coalesce(confidence, 0)
else source_confidence
end
where true;
with ranked_user_documents as (
select
id,
row_number() over (
partition by phone, doc_type
order by created_at desc, id desc
) as rn
from user_documents
where doc_type <> 'custom'
)
delete from user_documents
where id in (
select id from ranked_user_documents where rn > 1
);
create index if not exists user_documents_phone_idx on user_documents(phone);
create index if not exists user_documents_phone_type_idx on user_documents(phone, doc_type);
create index if not exists user_documents_phone_custom_label_idx on user_documents(phone, custom_label);
create index if not exists user_documents_created_idx on user_documents(created_at desc);
drop index if exists user_documents_phone_doc_type_unique_idx;
create unique index if not exists user_documents_phone_builtin_doc_type_unique_idx
on user_documents(phone, doc_type)
where doc_type <> 'custom';
-- ------------------------------------------------------------
-- 7b. document_access_logs — audit trail for preview/edit/delete/reveal
-- ------------------------------------------------------------
create table if not exists document_access_logs (
id uuid primary key default gen_random_uuid(),
document_id uuid,
phone text not null,
action text not null,
metadata jsonb not null default '{}',
created_at timestamptz not null default now()
);
create index if not exists document_access_logs_phone_idx on document_access_logs(phone);
create index if not exists document_access_logs_document_idx on document_access_logs(document_id);
create index if not exists document_access_logs_created_idx on document_access_logs(created_at desc);
-- ------------------------------------------------------------
-- 7c. Optional Supabase RLS / Storage policy scaffolding for beta
-- Run after confirming JWT phone claims in your hosted project.
-- ------------------------------------------------------------
-- alter table public.user_documents enable row level security;
-- drop policy if exists user_documents_owner_select on public.user_documents;
-- create policy user_documents_owner_select on public.user_documents
-- for select using ((auth.jwt() ->> 'phone') = phone);
-- drop policy if exists user_documents_owner_write on public.user_documents;
-- create policy user_documents_owner_write on public.user_documents
-- for all using ((auth.jwt() ->> 'phone') = phone)
-- with check ((auth.jwt() ->> 'phone') = phone);
--
-- alter table public.document_access_logs enable row level security;
-- drop policy if exists document_access_logs_owner_select on public.document_access_logs;
-- create policy document_access_logs_owner_select on public.document_access_logs
-- for select using ((auth.jwt() ->> 'phone') = phone);
--
-- insert into storage.buckets (id, name, public)
-- values ('user-documents', 'user-documents', false)
-- on conflict (id) do nothing;
-- drop policy if exists user_documents_storage_owner_read on storage.objects;
-- create policy user_documents_storage_owner_read on storage.objects
-- for select using (
-- bucket_id = 'user-documents'
-- and (storage.foldername(name))[1] = (auth.jwt() ->> 'phone')
-- );
-- ------------------------------------------------------------
-- 8. live_sessions — real-time form-fill progress
-- ------------------------------------------------------------
create table if not exists live_sessions (
session_id text primary key,
phone text not null,
portal text not null default 'nsp',
step integer not null default 1,
total_steps integer not null default 5,
form_state jsonb not null default '{}',
status text not null default 'in_progress',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists live_sessions_phone_idx on live_sessions(phone);
-- ------------------------------------------------------------
-- 8b. activity_feed — citizen-facing dashboard activity timeline
-- ------------------------------------------------------------
create table if not exists activity_feed (
id uuid primary key default gen_random_uuid(),
phone text not null,
event text not null,
created_at timestamptz not null default now()
);
create index if not exists activity_feed_phone_created_idx on activity_feed(phone, created_at desc);
-- ------------------------------------------------------------
-- 9. renewal_reminders — upcoming scholarship renewal alerts
-- ------------------------------------------------------------
create table if not exists renewal_reminders (
id uuid primary key default gen_random_uuid(),
phone text not null,
portal text not null,
renewal_due_date date not null,
sent_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (phone, portal)
);
create index if not exists renewal_reminders_due_idx on renewal_reminders(renewal_due_date);
-- ------------------------------------------------------------
-- 10. otp_rate_limits — cross-worker OTP rate limiting
-- One row per phone + purpose. window_start resets when > 10 min old.
-- ------------------------------------------------------------
create table if not exists otp_rate_limits (
phone text not null,
purpose text not null default 'login'
check (purpose in ('login', 'digilocker', 'bank_verify')),
request_count integer not null default 0,
window_start timestamptz not null default now(),
primary key (phone, purpose)
);
create table if not exists otp_verify_rate_limits (
phone text not null,
purpose text not null default 'login'
check (purpose in ('login', 'digilocker', 'bank_verify')),
request_count integer not null default 0,
window_start timestamptz not null default now(),
primary key (phone, purpose)
);
alter table if exists otp_rate_limits
add column if not exists purpose text not null default 'login';
alter table if exists otp_verify_rate_limits
add column if not exists purpose text not null default 'login';
alter table if exists otp_rate_limits
drop constraint if exists otp_rate_limits_pkey;
alter table if exists otp_rate_limits
add constraint otp_rate_limits_pkey primary key (phone, purpose);
alter table if exists otp_verify_rate_limits
drop constraint if exists otp_verify_rate_limits_pkey;
alter table if exists otp_verify_rate_limits
add constraint otp_verify_rate_limits_pkey primary key (phone, purpose);
do $$
begin
if not exists (
select 1
from pg_constraint
where conname = 'otp_rate_limits_purpose_check'
and conrelid = 'otp_rate_limits'::regclass
) then
alter table otp_rate_limits
add constraint otp_rate_limits_purpose_check
check (purpose in ('login', 'digilocker', 'bank_verify'));
end if;
if not exists (
select 1
from pg_constraint
where conname = 'otp_verify_rate_limits_purpose_check'
and conrelid = 'otp_verify_rate_limits'::regclass
) then
alter table otp_verify_rate_limits
add constraint otp_verify_rate_limits_purpose_check
check (purpose in ('login', 'digilocker', 'bank_verify'));
end if;
end $$;
create table if not exists login_handoffs (
id uuid primary key default gen_random_uuid(),
phone text not null,
code_hash text not null unique,
next_path text not null default '/dashboard',
expires_at timestamptz not null,
used boolean not null default false,
created_at timestamptz not null default now(),
used_at timestamptz
);
create index if not exists login_handoffs_phone_idx on login_handoffs(phone);
create index if not exists login_handoffs_expires_idx on login_handoffs(expires_at);
-- ------------------------------------------------------------
-- 9. fraud_flags — duplicate-Aadhaar detection
-- aadhaar_hash is SHA-256 of the 12-digit Aadhaar number.
-- ------------------------------------------------------------
create table if not exists fraud_flags (
id uuid primary key default gen_random_uuid(),
aadhaar_hash text not null unique,
phones text[] not null default '{}',
portal text not null default 'nsp',
flagged_at timestamptz not null default now()
);
create index if not exists fraud_flags_hash_idx on fraud_flags(aadhaar_hash);
-- ------------------------------------------------------------
-- 11. digilocker_consents — mock DigiLocker OAuth consent tracking
-- ------------------------------------------------------------
create table if not exists digilocker_consents (
id uuid primary key default gen_random_uuid(),
consent_id text not null unique,
phone text not null references sessions(phone),
status text not null default 'pending', -- pending, completed, rejected, expired
scope text[] not null default '{}',
redirect_url text,
documents_fetched integer default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
expires_at timestamptz default (now() + interval '30 minutes')
);
create index if not exists digilocker_consents_phone_idx on digilocker_consents(phone);
create index if not exists digilocker_consents_status_idx on digilocker_consents(status);
-- ------------------------------------------------------------
-- 12. digilocker_docs — mock documents fetched from DigiLocker
-- ------------------------------------------------------------
create table if not exists digilocker_docs (
id uuid primary key default gen_random_uuid(),
consent_id text references digilocker_consents(consent_id),
phone text not null references sessions(phone),
doc_type text not null, -- aadhaar, income_certificate, caste_certificate
name text not null,
digilocker_uri text not null,
size integer,
mime_type text default 'application/pdf',
raw_data text, -- base64 encoded document (mock)
extracted_data jsonb default '{}',
fetched_at timestamptz not null default now(),
used_in_application boolean default false
);
create index if not exists digilocker_docs_phone_idx on digilocker_docs(phone);
create index if not exists digilocker_docs_type_idx on digilocker_docs(doc_type);
-- ------------------------------------------------------------
-- 13. bank_verifications — NPCI bank account verification
-- ------------------------------------------------------------
create table if not exists bank_verifications (
id uuid primary key default gen_random_uuid(),
phone text not null references sessions(phone),
account_hash text not null, -- SHA256 of account number
account_last4 text, -- Last 4 digits for display
ifsc_code text not null,
status text not null default 'pending', -- pending, verified, failed
verified boolean default false,
beneficiary_name text,
account_status text,
mock_response jsonb default '{}',
error_message text,
verified_at timestamptz,
attempted_at timestamptz not null default now()
);
create index if not exists bank_verifications_phone_idx on bank_verifications(phone);
create index if not exists bank_verifications_status_idx on bank_verifications(status);
-- ------------------------------------------------------------
-- 14. disbursement_tracking — Scholarship disbursement status
-- ------------------------------------------------------------
create table if not exists disbursement_tracking (
id uuid primary key default gen_random_uuid(),
confirmation_number text references applications(confirmation_number),
phone text not null,
amount decimal(10, 2) not null,
bank_verification_id uuid references bank_verifications(id),
npci_txn_id text,
status text not null default 'pending', -- pending, processing, credited, failed
credited_at timestamptz,
notified boolean default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists disbursement_tracking_phone_idx on disbursement_tracking(phone);
create index if not exists disbursement_tracking_status_idx on disbursement_tracking(status);
create index if not exists disbursement_tracking_confirmation_idx on disbursement_tracking(confirmation_number);
-- ------------------------------------------------------------
-- 15. verifiable_credentials — Blockchain W3C credentials
-- ------------------------------------------------------------
create table if not exists verifiable_credentials (
id uuid primary key default gen_random_uuid(),
credential_id text not null unique,
confirmation_number text not null,
phone text not null references sessions(phone),
blockchain_tx_hash text not null,
credential_hash text not null,
ipfs_hash text,
credential_json jsonb not null,
issued_at timestamptz not null default now(),
revoked boolean default false,
revoked_at timestamptz,
revoke_reason text
);
create index if not exists verifiable_credentials_phone_idx on verifiable_credentials(phone);
create index if not exists verifiable_credentials_confirmation_idx on verifiable_credentials(confirmation_number);
create index if not exists verifiable_credentials_tx_hash_idx on verifiable_credentials(blockchain_tx_hash);
-- ------------------------------------------------------------
-- 15b. applications de-duplication — one active application per phone+portal
-- Keeps the latest submitted_at row and rewires dependent records.
-- ------------------------------------------------------------
with ranked_applications as (
select
id,
phone,
portal,
confirmation_number,
row_number() over (
partition by phone, portal
order by submitted_at desc, id desc
) as rn,
first_value(confirmation_number) over (
partition by phone, portal
order by submitted_at desc, id desc
) as keep_confirmation_number
from applications
)
update disbursement_tracking
set confirmation_number = ranked_applications.keep_confirmation_number
from ranked_applications
where ranked_applications.rn > 1
and disbursement_tracking.confirmation_number = ranked_applications.confirmation_number;
with ranked_applications as (
select
id,
phone,
portal,
confirmation_number,
row_number() over (
partition by phone, portal
order by submitted_at desc, id desc
) as rn,
first_value(confirmation_number) over (
partition by phone, portal
order by submitted_at desc, id desc
) as keep_confirmation_number
from applications
)
update verifiable_credentials
set confirmation_number = ranked_applications.keep_confirmation_number
from ranked_applications
where ranked_applications.rn > 1
and verifiable_credentials.confirmation_number = ranked_applications.confirmation_number;
with ranked_applications as (
select
id,
row_number() over (
partition by phone, portal
order by submitted_at desc, id desc
) as rn
from applications
)
delete from applications
where id in (
select id from ranked_applications where rn > 1
);
create unique index if not exists applications_phone_portal_unique_idx
on applications(phone, portal);
-- ------------------------------------------------------------
-- 17. citizen_profiles — persistent citizen profile (source of truth for auto-fill)
-- ------------------------------------------------------------
create table if not exists citizen_profiles (
phone text primary key,
full_name text,
dob date,
gender text,
pan_number text,
aadhaar_number text, -- full 12-digit; aadhaar_last4 is derived from this
aadhaar_last4 text,
address text,
state text,
district text,
pincode text,
income integer,
caste text, -- general / obc / sc / st / ews
religion text,
course_level text, -- eligibility enum: pre_matric / post_matric / degree / pg
course_name text, -- course as printed on the form, e.g. 'Information Science'
institution text,
board text,
academic_year text,
admission_date date,
marks_pct numeric(5,2),
bank_account text,
bank_ifsc text,
bank_name text,
bank_branch text,
father_name text,
mother_name text,
email text,
passkey_hash text,
digilocker_connected boolean default false,
profile_complete boolean default false,
updated_at timestamptz not null default now()
);
alter table if exists citizen_profiles add column if not exists pan_number text;
alter table if exists citizen_profiles add column if not exists passkey_hash text;
alter table if exists citizen_profiles add column if not exists aadhaar_number text;
alter table if exists citizen_profiles add column if not exists course_name text;
alter table if exists citizen_profiles add column if not exists board text;
alter table if exists citizen_profiles add column if not exists academic_year text;
alter table if exists citizen_profiles add column if not exists admission_date date;
alter table if exists citizen_profiles add column if not exists bank_branch text;
-- ------------------------------------------------------------
-- 18. form_fill_sessions — auto-fill history for any portal URL
-- ------------------------------------------------------------
create table if not exists form_fill_sessions (
id uuid primary key default gen_random_uuid(),
phone text not null,
url text not null,
field_map jsonb not null default '{}',
filled_count integer not null default 0,
missing_fields text[] not null default '{}',
screenshot_path text,
status text not null default 'pending', -- pending, filled, failed
created_at timestamptz not null default now()
);
create index if not exists form_fill_sessions_phone_idx on form_fill_sessions(phone);
-- ------------------------------------------------------------
-- 19. Realtime publication wiring for citizen dashboard updates
-- Safe to re-run; skips tables already attached to supabase_realtime.
-- ------------------------------------------------------------
do $$
declare
publication_exists boolean;
begin
select exists (
select 1
from pg_publication
where pubname = 'supabase_realtime'
) into publication_exists;
if not publication_exists then
return;
end if;
if not exists (
select 1
from pg_publication_rel pr
join pg_publication p on p.oid = pr.prpubid
join pg_class c on c.oid = pr.prrelid
join pg_namespace n on n.oid = c.relnamespace
where p.pubname = 'supabase_realtime'
and n.nspname = 'public'
and c.relname = 'applications'
) then
execute 'alter publication supabase_realtime add table public.applications';
end if;
if not exists (
select 1
from pg_publication_rel pr
join pg_publication p on p.oid = pr.prpubid
join pg_class c on c.oid = pr.prrelid
join pg_namespace n on n.oid = c.relnamespace
where p.pubname = 'supabase_realtime'
and n.nspname = 'public'
and c.relname = 'activity_feed'
) then
execute 'alter publication supabase_realtime add table public.activity_feed';
end if;
if not exists (
select 1
from pg_publication_rel pr
join pg_publication p on p.oid = pr.prpubid
join pg_class c on c.oid = pr.prrelid
join pg_namespace n on n.oid = c.relnamespace
where p.pubname = 'supabase_realtime'
and n.nspname = 'public'
and c.relname = 'citizen_profiles'
) then
execute 'alter publication supabase_realtime add table public.citizen_profiles';
end if;
end $$;