@realtimex/folio 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.env.example +20 -0
  2. package/README.md +63 -0
  3. package/api/server.ts +130 -0
  4. package/api/src/config/index.ts +96 -0
  5. package/api/src/middleware/auth.ts +128 -0
  6. package/api/src/middleware/errorHandler.ts +88 -0
  7. package/api/src/middleware/index.ts +4 -0
  8. package/api/src/middleware/rateLimit.ts +71 -0
  9. package/api/src/middleware/validation.ts +58 -0
  10. package/api/src/routes/accounts.ts +142 -0
  11. package/api/src/routes/baseline-config.ts +124 -0
  12. package/api/src/routes/chat.ts +154 -0
  13. package/api/src/routes/health.ts +61 -0
  14. package/api/src/routes/index.ts +35 -0
  15. package/api/src/routes/ingestions.ts +275 -0
  16. package/api/src/routes/migrate.ts +112 -0
  17. package/api/src/routes/policies.ts +121 -0
  18. package/api/src/routes/processing.ts +90 -0
  19. package/api/src/routes/rules.ts +11 -0
  20. package/api/src/routes/sdk.ts +100 -0
  21. package/api/src/routes/settings.ts +80 -0
  22. package/api/src/routes/setup.ts +389 -0
  23. package/api/src/routes/stats.ts +81 -0
  24. package/api/src/routes/tts.ts +190 -0
  25. package/api/src/services/BaselineConfigService.ts +208 -0
  26. package/api/src/services/ChatService.ts +204 -0
  27. package/api/src/services/GoogleDriveService.ts +331 -0
  28. package/api/src/services/GoogleSheetsService.ts +1107 -0
  29. package/api/src/services/IngestionService.ts +1187 -0
  30. package/api/src/services/ModelCapabilityService.ts +248 -0
  31. package/api/src/services/PolicyEngine.ts +1625 -0
  32. package/api/src/services/PolicyLearningService.ts +527 -0
  33. package/api/src/services/PolicyLoader.ts +249 -0
  34. package/api/src/services/RAGService.ts +391 -0
  35. package/api/src/services/SDKService.ts +249 -0
  36. package/api/src/services/supabase.ts +113 -0
  37. package/api/src/utils/Actuator.ts +284 -0
  38. package/api/src/utils/actions/ActionHandler.ts +34 -0
  39. package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
  40. package/api/src/utils/actions/AutoRenameAction.ts +58 -0
  41. package/api/src/utils/actions/CopyAction.ts +120 -0
  42. package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
  43. package/api/src/utils/actions/LogCsvAction.ts +48 -0
  44. package/api/src/utils/actions/NotifyAction.ts +39 -0
  45. package/api/src/utils/actions/RenameAction.ts +57 -0
  46. package/api/src/utils/actions/WebhookAction.ts +58 -0
  47. package/api/src/utils/actions/utils.ts +293 -0
  48. package/api/src/utils/llmResponse.ts +61 -0
  49. package/api/src/utils/logger.ts +67 -0
  50. package/bin/folio-deploy.js +12 -0
  51. package/bin/folio-setup.js +45 -0
  52. package/bin/folio.js +65 -0
  53. package/dist/api/server.js +106 -0
  54. package/dist/api/src/config/index.js +81 -0
  55. package/dist/api/src/middleware/auth.js +93 -0
  56. package/dist/api/src/middleware/errorHandler.js +73 -0
  57. package/dist/api/src/middleware/index.js +4 -0
  58. package/dist/api/src/middleware/rateLimit.js +43 -0
  59. package/dist/api/src/middleware/validation.js +54 -0
  60. package/dist/api/src/routes/accounts.js +110 -0
  61. package/dist/api/src/routes/baseline-config.js +91 -0
  62. package/dist/api/src/routes/chat.js +114 -0
  63. package/dist/api/src/routes/health.js +52 -0
  64. package/dist/api/src/routes/index.js +31 -0
  65. package/dist/api/src/routes/ingestions.js +207 -0
  66. package/dist/api/src/routes/migrate.js +91 -0
  67. package/dist/api/src/routes/policies.js +86 -0
  68. package/dist/api/src/routes/processing.js +75 -0
  69. package/dist/api/src/routes/rules.js +8 -0
  70. package/dist/api/src/routes/sdk.js +80 -0
  71. package/dist/api/src/routes/settings.js +68 -0
  72. package/dist/api/src/routes/setup.js +315 -0
  73. package/dist/api/src/routes/stats.js +62 -0
  74. package/dist/api/src/routes/tts.js +178 -0
  75. package/dist/api/src/services/BaselineConfigService.js +168 -0
  76. package/dist/api/src/services/ChatService.js +166 -0
  77. package/dist/api/src/services/GoogleDriveService.js +280 -0
  78. package/dist/api/src/services/GoogleSheetsService.js +795 -0
  79. package/dist/api/src/services/IngestionService.js +990 -0
  80. package/dist/api/src/services/ModelCapabilityService.js +179 -0
  81. package/dist/api/src/services/PolicyEngine.js +1353 -0
  82. package/dist/api/src/services/PolicyLearningService.js +397 -0
  83. package/dist/api/src/services/PolicyLoader.js +159 -0
  84. package/dist/api/src/services/RAGService.js +295 -0
  85. package/dist/api/src/services/SDKService.js +212 -0
  86. package/dist/api/src/services/supabase.js +72 -0
  87. package/dist/api/src/utils/Actuator.js +225 -0
  88. package/dist/api/src/utils/actions/ActionHandler.js +1 -0
  89. package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
  90. package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
  91. package/dist/api/src/utils/actions/CopyAction.js +112 -0
  92. package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
  93. package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
  94. package/dist/api/src/utils/actions/NotifyAction.js +32 -0
  95. package/dist/api/src/utils/actions/RenameAction.js +51 -0
  96. package/dist/api/src/utils/actions/WebhookAction.js +51 -0
  97. package/dist/api/src/utils/actions/utils.js +237 -0
  98. package/dist/api/src/utils/llmResponse.js +63 -0
  99. package/dist/api/src/utils/logger.js +51 -0
  100. package/dist/assets/index-DzN8-j-e.css +1 -0
  101. package/dist/assets/index-Uy-ai3Dh.js +113 -0
  102. package/dist/favicon.svg +31 -0
  103. package/dist/folio-logo.svg +46 -0
  104. package/dist/index.html +14 -0
  105. package/docs-dev/FPE-spec.md +196 -0
  106. package/docs-dev/folio-prd.md +47 -0
  107. package/docs-dev/foundation-checklist.md +30 -0
  108. package/docs-dev/hybrid-routing-architecture.md +205 -0
  109. package/docs-dev/ingestion-engine.md +69 -0
  110. package/docs-dev/port-from-email-automator.md +32 -0
  111. package/docs-dev/tech-spec.md +98 -0
  112. package/index.html +13 -0
  113. package/package.json +101 -0
  114. package/public/favicon.svg +31 -0
  115. package/public/folio-logo.svg +46 -0
  116. package/scripts/dev-task.mjs +51 -0
  117. package/scripts/get-latest-migration-timestamp.mjs +34 -0
  118. package/scripts/migrate.sh +91 -0
  119. package/supabase/.temp/cli-latest +1 -0
  120. package/supabase/.temp/gotrue-version +1 -0
  121. package/supabase/.temp/pooler-url +1 -0
  122. package/supabase/.temp/postgres-version +1 -0
  123. package/supabase/.temp/project-ref +1 -0
  124. package/supabase/.temp/rest-version +1 -0
  125. package/supabase/.temp/storage-migration +1 -0
  126. package/supabase/.temp/storage-version +1 -0
  127. package/supabase/config.toml +64 -0
  128. package/supabase/functions/_shared/auth.ts +35 -0
  129. package/supabase/functions/_shared/cors.ts +12 -0
  130. package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
  131. package/supabase/functions/api-v1-settings/index.ts +66 -0
  132. package/supabase/functions/setup/index.ts +91 -0
  133. package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
  134. package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
  135. package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
  136. package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
  137. package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
  138. package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
  139. package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
  140. package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
  141. package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
  142. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
  143. package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
  144. package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
  145. package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
  146. package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
  147. package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
  148. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
  149. package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
  150. package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
  151. package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
  152. package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
  153. package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
  154. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
  155. package/supabase/migrations/29991231235959_test_migration.sql +0 -0
  156. package/supabase/templates/confirmation.html +76 -0
  157. package/supabase/templates/email-change.html +76 -0
  158. package/supabase/templates/invite.html +72 -0
  159. package/supabase/templates/magic-link.html +68 -0
  160. package/supabase/templates/recovery.html +82 -0
  161. package/tsconfig.api.json +16 -0
  162. package/tsconfig.json +25 -0
  163. package/vite.config.ts +146 -0
@@ -0,0 +1,66 @@
1
+ import { corsHeaders } from "../_shared/cors.ts";
2
+ import { authenticate } from "../_shared/auth.ts";
3
+
4
+ Deno.serve(async (req: Request) => {
5
+ if (req.method === "OPTIONS") {
6
+ return new Response("ok", { headers: corsHeaders });
7
+ }
8
+
9
+ try {
10
+ const { user, client } = await authenticate(req);
11
+
12
+ if (req.method === "GET") {
13
+ const { data, error } = await client
14
+ .from("user_settings")
15
+ .select("*")
16
+ .eq("user_id", user.id)
17
+ .maybeSingle();
18
+
19
+ if (error) {
20
+ return Response.json({ error: error.message }, { status: 500, headers: corsHeaders });
21
+ }
22
+
23
+ return Response.json({ settings: data }, { headers: corsHeaders });
24
+ }
25
+
26
+ if (req.method === "PATCH") {
27
+ const body = await req.json();
28
+ const rawVisionMap = body.vision_model_capabilities;
29
+ const payload = {
30
+ llm_provider: body.llm_provider,
31
+ llm_model: body.llm_model,
32
+ sync_interval_minutes: body.sync_interval_minutes,
33
+ tts_auto_play: body.tts_auto_play,
34
+ tts_provider: body.tts_provider,
35
+ tts_voice: body.tts_voice,
36
+ tts_speed: body.tts_speed,
37
+ tts_quality: body.tts_quality,
38
+ embedding_provider: body.embedding_provider,
39
+ embedding_model: body.embedding_model,
40
+ storage_path: body.storage_path,
41
+ vision_model_capabilities: rawVisionMap && typeof rawVisionMap === "object" && !Array.isArray(rawVisionMap)
42
+ ? rawVisionMap
43
+ : undefined
44
+ };
45
+
46
+ const { data, error } = await client
47
+ .from("user_settings")
48
+ .upsert({ user_id: user.id, ...payload }, { onConflict: "user_id" })
49
+ .select("*")
50
+ .single();
51
+
52
+ if (error) {
53
+ return Response.json({ error: error.message }, { status: 500, headers: corsHeaders });
54
+ }
55
+
56
+ return Response.json({ settings: data }, { headers: corsHeaders });
57
+ }
58
+
59
+ return Response.json({ error: "Method not allowed" }, { status: 405, headers: corsHeaders });
60
+ } catch (error) {
61
+ return Response.json(
62
+ { error: error instanceof Error ? error.message : "Unexpected error" },
63
+ { status: 401, headers: corsHeaders }
64
+ );
65
+ }
66
+ });
@@ -0,0 +1,91 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { getAdminClient } from "../_shared/supabaseAdmin.ts";
3
+ import { corsHeaders, createErrorResponse } from "../_shared/cors.ts";
4
+
5
+ async function createFirstUser(req: Request) {
6
+ try {
7
+ const { email, password, first_name, last_name } = await req.json();
8
+ console.log(`[Setup] Starting setup for ${email}`);
9
+
10
+ const supabaseAdmin = getAdminClient();
11
+
12
+ // Check if any users exist
13
+ const { count, error: countError } = await supabaseAdmin
14
+ .from("profiles")
15
+ .select("*", { count: "exact", head: true });
16
+
17
+ if (countError) {
18
+ console.error("[Setup] Error checking profiles table:", countError);
19
+ return createErrorResponse(500, `Database error checking profiles: ${countError.message} (code: ${countError.code})`);
20
+ }
21
+
22
+ console.log(`[Setup] Existing profiles count: ${count}`);
23
+ if (count && count > 0) {
24
+ return createErrorResponse(403, "First user already exists");
25
+ }
26
+
27
+ // Create user with admin API
28
+ console.log("[Setup] Creating admin user...");
29
+ const { data, error: userError } = await supabaseAdmin.auth.admin.createUser({
30
+ email,
31
+ password,
32
+ email_confirm: true,
33
+ user_metadata: { first_name, last_name },
34
+ });
35
+
36
+ if (userError || !data?.user) {
37
+ console.error("[Setup] Error creating auth user:", userError);
38
+ return createErrorResponse(500, `Failed to create auth user: ${userError?.message || 'Unknown error'}`);
39
+ }
40
+
41
+ console.log(`[Setup] User created successfully: ${data.user.id}. Creating profile...`);
42
+
43
+ // Explicitly create profile as admin
44
+ const { error: profileError } = await supabaseAdmin
45
+ .from("profiles")
46
+ .upsert({
47
+ id: data.user.id,
48
+ email: data.user.email,
49
+ first_name: first_name || null,
50
+ last_name: last_name || null,
51
+ is_admin: true,
52
+ }, { onConflict: 'id' });
53
+
54
+ if (profileError) {
55
+ console.error("[Setup] Error creating profile row:", profileError);
56
+ return createErrorResponse(500, `User created but profile record failed: ${profileError.message} (code: ${profileError.code})`);
57
+ }
58
+
59
+ console.log("[Setup] Setup completed successfully");
60
+
61
+ return new Response(
62
+ JSON.stringify({
63
+ data: {
64
+ id: data.user.id,
65
+ email: data.user.email,
66
+ },
67
+ }),
68
+ {
69
+ headers: { "Content-Type": "application/json", ...corsHeaders },
70
+ },
71
+ );
72
+ } catch (error) {
73
+ console.error("Unexpected error in createFirstUser:", error);
74
+ return createErrorResponse(500, `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
75
+ }
76
+ }
77
+
78
+ Deno.serve(async (req: Request) => {
79
+ if (req.method === "OPTIONS") {
80
+ return new Response(null, {
81
+ status: 204,
82
+ headers: corsHeaders,
83
+ });
84
+ }
85
+
86
+ if (req.method === "POST") {
87
+ return createFirstUser(req);
88
+ }
89
+
90
+ return createErrorResponse(405, "Method Not Allowed");
91
+ });
@@ -0,0 +1,136 @@
1
+ -- Folio foundation schema
2
+
3
+ create table if not exists public.profiles (
4
+ id uuid primary key references auth.users(id) on delete cascade,
5
+ first_name text,
6
+ last_name text,
7
+ email text,
8
+ avatar_url text,
9
+ is_admin boolean default false,
10
+ created_at timestamptz default now(),
11
+ updated_at timestamptz default now()
12
+ );
13
+
14
+ create table if not exists public.user_settings (
15
+ id uuid primary key default gen_random_uuid(),
16
+ user_id uuid not null references auth.users(id) on delete cascade unique,
17
+ llm_provider text,
18
+ llm_model text,
19
+ sync_interval_minutes integer not null default 5 check (sync_interval_minutes >= 1 and sync_interval_minutes <= 60),
20
+ created_at timestamptz default now(),
21
+ updated_at timestamptz default now()
22
+ );
23
+
24
+ create table if not exists public.integrations (
25
+ id uuid primary key default gen_random_uuid(),
26
+ user_id uuid not null references auth.users(id) on delete cascade,
27
+ provider text not null,
28
+ credentials jsonb not null default '{}'::jsonb,
29
+ is_enabled boolean not null default true,
30
+ created_at timestamptz default now(),
31
+ updated_at timestamptz default now(),
32
+ unique (user_id, provider)
33
+ );
34
+
35
+ create table if not exists public.processing_jobs (
36
+ id uuid primary key default gen_random_uuid(),
37
+ user_id uuid not null references auth.users(id) on delete cascade,
38
+ status text not null check (status in ('queued', 'running', 'completed', 'failed')),
39
+ source_type text not null,
40
+ payload jsonb not null default '{}'::jsonb,
41
+ runtime_key text,
42
+ error_message text,
43
+ created_at timestamptz default now(),
44
+ updated_at timestamptz default now()
45
+ );
46
+
47
+ create table if not exists public.system_logs (
48
+ id uuid primary key default gen_random_uuid(),
49
+ user_id uuid references auth.users(id) on delete cascade,
50
+ level text not null,
51
+ scope text not null,
52
+ message text not null,
53
+ metadata jsonb not null default '{}'::jsonb,
54
+ created_at timestamptz default now()
55
+ );
56
+
57
+ alter table public.profiles enable row level security;
58
+ alter table public.user_settings enable row level security;
59
+ alter table public.integrations enable row level security;
60
+ alter table public.processing_jobs enable row level security;
61
+ alter table public.system_logs enable row level security;
62
+
63
+ create policy "profiles own rows" on public.profiles
64
+ for all using (auth.uid() = id);
65
+
66
+ create policy "user_settings own rows" on public.user_settings
67
+ for all using (auth.uid() = user_id);
68
+
69
+ create policy "integrations own rows" on public.integrations
70
+ for all using (auth.uid() = user_id);
71
+
72
+ create policy "processing_jobs own rows" on public.processing_jobs
73
+ for all using (auth.uid() = user_id);
74
+
75
+ create policy "system_logs own rows" on public.system_logs
76
+ for all using (auth.uid() = user_id);
77
+
78
+ create index if not exists idx_user_settings_user_id on public.user_settings(user_id);
79
+ create index if not exists idx_integrations_user_id on public.integrations(user_id);
80
+ create index if not exists idx_processing_jobs_user_id on public.processing_jobs(user_id);
81
+ create index if not exists idx_processing_jobs_status on public.processing_jobs(status);
82
+ create index if not exists idx_system_logs_user_id_created_at on public.system_logs(user_id, created_at desc);
83
+
84
+ create or replace function public.update_updated_at_column()
85
+ returns trigger
86
+ language plpgsql
87
+ as $$
88
+ begin
89
+ new.updated_at = now();
90
+ return new;
91
+ end;
92
+ $$;
93
+
94
+ drop trigger if exists update_profiles_updated_at on public.profiles;
95
+ create trigger update_profiles_updated_at
96
+ before update on public.profiles
97
+ for each row execute function public.update_updated_at_column();
98
+
99
+ drop trigger if exists update_user_settings_updated_at on public.user_settings;
100
+ create trigger update_user_settings_updated_at
101
+ before update on public.user_settings
102
+ for each row execute function public.update_updated_at_column();
103
+
104
+ drop trigger if exists update_integrations_updated_at on public.integrations;
105
+ create trigger update_integrations_updated_at
106
+ before update on public.integrations
107
+ for each row execute function public.update_updated_at_column();
108
+
109
+ drop trigger if exists update_processing_jobs_updated_at on public.processing_jobs;
110
+ create trigger update_processing_jobs_updated_at
111
+ before update on public.processing_jobs
112
+ for each row execute function public.update_updated_at_column();
113
+
114
+ create or replace function public.handle_new_user()
115
+ returns trigger
116
+ language plpgsql
117
+ security definer
118
+ set search_path = public
119
+ as $$
120
+ begin
121
+ insert into public.profiles (id, email)
122
+ values (new.id, new.email)
123
+ on conflict (id) do nothing;
124
+
125
+ insert into public.user_settings (user_id)
126
+ values (new.id)
127
+ on conflict (user_id) do nothing;
128
+
129
+ return new;
130
+ end;
131
+ $$;
132
+
133
+ drop trigger if exists on_auth_user_created on auth.users;
134
+ create trigger on_auth_user_created
135
+ after insert on auth.users
136
+ for each row execute procedure public.handle_new_user();
@@ -0,0 +1,10 @@
1
+ create or replace function public.get_latest_migration_timestamp()
2
+ returns text
3
+ language sql
4
+ security definer
5
+ set search_path = ''
6
+ as $$
7
+ select max(version) from supabase_migrations.schema_migrations;
8
+ $$;
9
+
10
+ grant execute on function public.get_latest_migration_timestamp() to anon, authenticated;
@@ -0,0 +1,20 @@
1
+ -- Init-state parity with email-automator, adapted for Folio.
2
+ -- In Folio, user presence is represented by public.profiles.
3
+
4
+ create or replace view public.init_state
5
+ with (security_invoker=off)
6
+ as
7
+ select
8
+ count(id) as is_initialized
9
+ from
10
+ (
11
+ select
12
+ profiles.id
13
+ from
14
+ public.profiles
15
+ limit
16
+ 1
17
+ ) sub;
18
+
19
+ grant usage on schema public to anon, authenticated;
20
+ grant select on public.init_state to anon, authenticated;
@@ -0,0 +1,139 @@
1
+ -- Port user creation parity from email-automator (foundation only).
2
+ -- This keeps Folio profile bootstrap synced with auth.users lifecycle.
3
+
4
+ create or replace function public.handle_new_auth_user()
5
+ returns trigger
6
+ language plpgsql
7
+ security definer
8
+ set search_path = 'pg_catalog', 'public'
9
+ as $$
10
+ declare
11
+ should_be_admin boolean;
12
+ begin
13
+ -- Serialize first-user admin assignment to avoid races.
14
+ perform pg_advisory_xact_lock(602240003);
15
+
16
+ select not exists (
17
+ select 1
18
+ from public.profiles p
19
+ where p.is_admin = true
20
+ )
21
+ into should_be_admin;
22
+
23
+ insert into public.profiles (id, first_name, last_name, email, is_admin)
24
+ values (
25
+ new.id,
26
+ new.raw_user_meta_data ->> 'first_name',
27
+ new.raw_user_meta_data ->> 'last_name',
28
+ new.email,
29
+ should_be_admin
30
+ )
31
+ on conflict (id) do update
32
+ set
33
+ first_name = coalesce(excluded.first_name, public.profiles.first_name),
34
+ last_name = coalesce(excluded.last_name, public.profiles.last_name),
35
+ email = excluded.email,
36
+ updated_at = now();
37
+
38
+ insert into public.user_settings (user_id)
39
+ values (new.id)
40
+ on conflict (user_id) do nothing;
41
+
42
+ return new;
43
+ end;
44
+ $$;
45
+
46
+ create or replace function public.handle_update_auth_user()
47
+ returns trigger
48
+ language plpgsql
49
+ security definer
50
+ set search_path = 'pg_catalog', 'public'
51
+ as $$
52
+ begin
53
+ update public.profiles
54
+ set
55
+ first_name = coalesce(new.raw_user_meta_data ->> 'first_name', first_name),
56
+ last_name = coalesce(new.raw_user_meta_data ->> 'last_name', last_name),
57
+ email = new.email,
58
+ updated_at = now()
59
+ where id = new.id;
60
+
61
+ return new;
62
+ end;
63
+ $$;
64
+
65
+ drop trigger if exists on_auth_user_created on auth.users;
66
+ create trigger on_auth_user_created
67
+ after insert on auth.users
68
+ for each row execute function public.handle_new_auth_user();
69
+
70
+ drop trigger if exists on_auth_user_updated on auth.users;
71
+ create trigger on_auth_user_updated
72
+ after update on auth.users
73
+ for each row
74
+ when (
75
+ old.email is distinct from new.email
76
+ or old.raw_user_meta_data is distinct from new.raw_user_meta_data
77
+ )
78
+ execute function public.handle_update_auth_user();
79
+
80
+ -- Backfill profiles for existing auth.users rows where trigger might not have run.
81
+ insert into public.profiles (id, first_name, last_name, email, is_admin, created_at, updated_at)
82
+ select
83
+ u.id,
84
+ u.raw_user_meta_data ->> 'first_name',
85
+ u.raw_user_meta_data ->> 'last_name',
86
+ u.email,
87
+ false,
88
+ coalesce(u.created_at, now()),
89
+ now()
90
+ from auth.users u
91
+ where not exists (
92
+ select 1
93
+ from public.profiles p
94
+ where p.id = u.id
95
+ );
96
+
97
+ -- Ensure exactly one bootstrap admin exists for legacy projects.
98
+ with admin_candidate as (
99
+ select p.id
100
+ from public.profiles p
101
+ left join auth.users u on u.id = p.id
102
+ order by coalesce(u.created_at, p.created_at), p.id
103
+ limit 1
104
+ )
105
+ update public.profiles p
106
+ set
107
+ is_admin = true,
108
+ updated_at = now()
109
+ where p.id in (select id from admin_candidate)
110
+ and not exists (
111
+ select 1
112
+ from public.profiles existing_admin
113
+ where existing_admin.is_admin = true
114
+ );
115
+
116
+ -- Keep profile identity fields synchronized for existing users.
117
+ update public.profiles p
118
+ set
119
+ first_name = coalesce(u.raw_user_meta_data ->> 'first_name', p.first_name),
120
+ last_name = coalesce(u.raw_user_meta_data ->> 'last_name', p.last_name),
121
+ email = u.email,
122
+ updated_at = now()
123
+ from auth.users u
124
+ where u.id = p.id
125
+ and (
126
+ p.email is distinct from u.email
127
+ or p.first_name is distinct from (u.raw_user_meta_data ->> 'first_name')
128
+ or p.last_name is distinct from (u.raw_user_meta_data ->> 'last_name')
129
+ );
130
+
131
+ -- Backfill user_settings for existing users if missing.
132
+ insert into public.user_settings (user_id)
133
+ select u.id
134
+ from auth.users u
135
+ where not exists (
136
+ select 1
137
+ from public.user_settings s
138
+ where s.user_id = u.id
139
+ );
@@ -0,0 +1,26 @@
1
+ -- Storage bucket for user avatars
2
+ insert into storage.buckets (id, name, public)
3
+ values ('avatars', 'avatars', true)
4
+ on conflict (id) do nothing;
5
+
6
+ -- RLS policies for avatars bucket
7
+ create policy "Avatar upload" on storage.objects
8
+ for insert with check (
9
+ bucket_id = 'avatars'
10
+ and (storage.foldername(name))[1] = auth.uid()::text
11
+ );
12
+
13
+ create policy "Avatar update" on storage.objects
14
+ for update with check (
15
+ bucket_id = 'avatars'
16
+ and (storage.foldername(name))[1] = auth.uid()::text
17
+ );
18
+
19
+ create policy "Avatar delete" on storage.objects
20
+ for delete using (
21
+ bucket_id = 'avatars'
22
+ and (storage.foldername(name))[1] = auth.uid()::text
23
+ );
24
+
25
+ create policy "Avatar public access" on storage.objects
26
+ for select using (bucket_id = 'avatars');
@@ -0,0 +1,24 @@
1
+ -- Migration: Add TTS and Embedding settings to user_settings
2
+ -- Created: 2026-02-24
3
+
4
+ -- Add TTS settings
5
+ ALTER TABLE public.user_settings
6
+ ADD COLUMN IF NOT EXISTS tts_auto_play BOOLEAN DEFAULT false,
7
+ ADD COLUMN IF NOT EXISTS tts_provider TEXT DEFAULT 'piper_local',
8
+ ADD COLUMN IF NOT EXISTS tts_voice TEXT DEFAULT NULL,
9
+ ADD COLUMN IF NOT EXISTS tts_speed NUMERIC DEFAULT 1.0,
10
+ ADD COLUMN IF NOT EXISTS tts_quality INTEGER DEFAULT 10;
11
+
12
+ -- Add Embedding settings
13
+ ALTER TABLE public.user_settings
14
+ ADD COLUMN IF NOT EXISTS embedding_provider TEXT DEFAULT 'realtimexai',
15
+ ADD COLUMN IF NOT EXISTS embedding_model TEXT DEFAULT 'text-embedding-3-small';
16
+
17
+ -- Add comments for documentation
18
+ COMMENT ON COLUMN public.user_settings.tts_auto_play IS 'Automatically read AI responses aloud using text-to-speech';
19
+ COMMENT ON COLUMN public.user_settings.tts_provider IS 'TTS provider (piper_local, supertonic_local, etc.)';
20
+ COMMENT ON COLUMN public.user_settings.tts_voice IS 'Voice ID specific to the selected provider';
21
+ COMMENT ON COLUMN public.user_settings.tts_speed IS 'Speech speed (0.5x to 2.0x)';
22
+ COMMENT ON COLUMN public.user_settings.tts_quality IS 'Audio quality/bitrate (1-20, higher = better quality)';
23
+ COMMENT ON COLUMN public.user_settings.embedding_provider IS 'Default embedding provider for RAG and search';
24
+ COMMENT ON COLUMN public.user_settings.embedding_model IS 'Default embedding model name';
@@ -0,0 +1,48 @@
1
+ -- Create policies table for user-managed FPE policies
2
+ CREATE TABLE IF NOT EXISTS policies (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
5
+ policy_id TEXT NOT NULL, -- the metadata.id from the YAML (e.g. "tesla-invoice-handler")
6
+ metadata JSONB NOT NULL,
7
+ spec JSONB NOT NULL,
8
+ enabled BOOLEAN NOT NULL DEFAULT true,
9
+ priority INTEGER NOT NULL DEFAULT 100,
10
+ api_version TEXT NOT NULL DEFAULT 'folio/v1',
11
+ kind TEXT NOT NULL DEFAULT 'Policy',
12
+ created_at TIMESTAMPTZ DEFAULT NOW(),
13
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
14
+ UNIQUE(user_id, policy_id)
15
+ );
16
+
17
+ -- Enable RLS
18
+ ALTER TABLE policies ENABLE ROW LEVEL SECURITY;
19
+
20
+ -- Users can only manage their own policies
21
+ CREATE POLICY "Users can read own policies"
22
+ ON policies FOR SELECT
23
+ USING (auth.uid() = user_id);
24
+
25
+ CREATE POLICY "Users can insert own policies"
26
+ ON policies FOR INSERT
27
+ WITH CHECK (auth.uid() = user_id);
28
+
29
+ CREATE POLICY "Users can update own policies"
30
+ ON policies FOR UPDATE
31
+ USING (auth.uid() = user_id);
32
+
33
+ CREATE POLICY "Users can delete own policies"
34
+ ON policies FOR DELETE
35
+ USING (auth.uid() = user_id);
36
+
37
+ -- Auto-update updated_at
38
+ CREATE OR REPLACE FUNCTION update_policies_updated_at()
39
+ RETURNS TRIGGER AS $$
40
+ BEGIN
41
+ NEW.updated_at = NOW();
42
+ RETURN NEW;
43
+ END;
44
+ $$ LANGUAGE plpgsql;
45
+
46
+ CREATE TRIGGER policies_updated_at
47
+ BEFORE UPDATE ON policies
48
+ FOR EACH ROW EXECUTE FUNCTION update_policies_updated_at();
@@ -0,0 +1,9 @@
1
+ -- Fix get_latest_migration_timestamp() to exclude sentinel/test migrations
2
+ -- Migrations with timestamps >= 29990000000000 are development-only sentinels
3
+ -- and should not influence the migration version comparison logic.
4
+ CREATE OR REPLACE FUNCTION get_latest_migration_timestamp()
5
+ RETURNS text AS $$
6
+ SELECT max(version)::text
7
+ FROM supabase_migrations.schema_migrations
8
+ WHERE version < '29990000000000';
9
+ $$ LANGUAGE sql SECURITY DEFINER;
@@ -0,0 +1,42 @@
1
+ -- Ingestion log table: tracks every document processed through Folio's Policy Engine
2
+ CREATE TABLE IF NOT EXISTS ingestions (
3
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
5
+ source text NOT NULL DEFAULT 'upload', -- 'upload' | 'dropzone' | 'email' | 'url'
6
+ filename text NOT NULL,
7
+ mime_type text,
8
+ file_size bigint,
9
+ status text NOT NULL DEFAULT 'pending', -- 'pending' | 'processing' | 'matched' | 'no_match' | 'error'
10
+ policy_id text, -- matched policy id (nullable)
11
+ policy_name text, -- denormalised for display
12
+ extracted jsonb DEFAULT '{}'::jsonb, -- key/value pairs extracted by FPE
13
+ actions_taken jsonb DEFAULT '[]'::jsonb, -- list of actions executed
14
+ error_message text,
15
+ storage_path text, -- supabase storage path (if file stored)
16
+ created_at timestamptz NOT NULL DEFAULT now(),
17
+ updated_at timestamptz NOT NULL DEFAULT now()
18
+ );
19
+
20
+ -- Index for per-user list queries
21
+ CREATE INDEX IF NOT EXISTS ingestions_user_id_idx ON ingestions(user_id, created_at DESC);
22
+
23
+ -- RLS
24
+ ALTER TABLE ingestions ENABLE ROW LEVEL SECURITY;
25
+
26
+ CREATE POLICY "Users can manage their own ingestions"
27
+ ON ingestions FOR ALL
28
+ USING (auth.uid() = user_id)
29
+ WITH CHECK (auth.uid() = user_id);
30
+
31
+ -- updated_at trigger
32
+ CREATE OR REPLACE FUNCTION update_ingestions_updated_at()
33
+ RETURNS TRIGGER AS $$
34
+ BEGIN
35
+ NEW.updated_at = now();
36
+ RETURN NEW;
37
+ END;
38
+ $$ LANGUAGE plpgsql;
39
+
40
+ CREATE TRIGGER ingestions_updated_at
41
+ BEFORE UPDATE ON ingestions
42
+ FOR EACH ROW EXECUTE FUNCTION update_ingestions_updated_at();