first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# Supabase
.branches
.temp
.env
+30
View File
@@ -0,0 +1,30 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "cadam"
[auth.email]
enable_signup = true
[auth.external.google]
enabled = true
client_id = "env(GOOGLE_CLIENT_ID)"
secret = "env(GOOGLE_SECRET)"
[storage.buckets.meshes]
public = false
[storage.buckets.images]
public = false
[storage.buckets.previews]
public = false
[db.migrations]
schema_paths = [
"./schemas/types.sql",
"./schemas/conversations.sql",
"./schemas/prompts.sql",
"./schemas/*.sql",
]
+7
View File
@@ -0,0 +1,7 @@
{
"lint": {
"rules": {
"exclude": ["no-import-prefix", "no-unversioned-import"]
}
}
}
@@ -0,0 +1,213 @@
create table "public"."conversations" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone default now(),
"updated_at" timestamp with time zone default now(),
"user_id" uuid not null,
"title" text not null,
"current_message_leaf_id" uuid
);
alter table "public"."conversations" enable row level security;
create table "public"."messages" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"conversation_id" uuid not null,
"role" text not null,
"content" jsonb not null,
"parent_message_id" uuid
);
alter table "public"."messages" enable row level security;
CREATE INDEX conversations_created_at_idx ON public.conversations USING btree (created_at);
CREATE UNIQUE INDEX conversations_pkey ON public.conversations USING btree (id);
CREATE INDEX conversations_updated_at_idx ON public.conversations USING btree (updated_at);
CREATE INDEX conversations_user_id_idx ON public.conversations USING btree (user_id);
CREATE INDEX messages_conversation_id_idx ON public.messages USING btree (conversation_id);
CREATE UNIQUE INDEX messages_pkey ON public.messages USING btree (id);
alter table "public"."conversations" add constraint "conversations_pkey" PRIMARY KEY using index "conversations_pkey";
alter table "public"."messages" add constraint "messages_pkey" PRIMARY KEY using index "messages_pkey";
alter table "public"."conversations" add constraint "conversations_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."conversations" validate constraint "conversations_user_id_fkey";
alter table "public"."messages" add constraint "messages_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE not valid;
alter table "public"."messages" validate constraint "messages_conversation_id_fkey";
alter table "public"."messages" add constraint "messages_role_check" CHECK ((role = ANY (ARRAY['user'::text, 'assistant'::text]))) not valid;
alter table "public"."messages" validate constraint "messages_role_check";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.update_conversation_leaf()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
begin
update conversations set
current_message_leaf_id = new.id,
updated_at = now()
where id = new.conversation_id;
return new;
end;
$function$
;
grant delete on table "public"."conversations" to "anon";
grant insert on table "public"."conversations" to "anon";
grant references on table "public"."conversations" to "anon";
grant select on table "public"."conversations" to "anon";
grant trigger on table "public"."conversations" to "anon";
grant truncate on table "public"."conversations" to "anon";
grant update on table "public"."conversations" to "anon";
grant delete on table "public"."conversations" to "authenticated";
grant insert on table "public"."conversations" to "authenticated";
grant references on table "public"."conversations" to "authenticated";
grant select on table "public"."conversations" to "authenticated";
grant trigger on table "public"."conversations" to "authenticated";
grant truncate on table "public"."conversations" to "authenticated";
grant update on table "public"."conversations" to "authenticated";
grant delete on table "public"."conversations" to "service_role";
grant insert on table "public"."conversations" to "service_role";
grant references on table "public"."conversations" to "service_role";
grant select on table "public"."conversations" to "service_role";
grant trigger on table "public"."conversations" to "service_role";
grant truncate on table "public"."conversations" to "service_role";
grant update on table "public"."conversations" to "service_role";
grant delete on table "public"."messages" to "anon";
grant insert on table "public"."messages" to "anon";
grant references on table "public"."messages" to "anon";
grant select on table "public"."messages" to "anon";
grant trigger on table "public"."messages" to "anon";
grant truncate on table "public"."messages" to "anon";
grant update on table "public"."messages" to "anon";
grant delete on table "public"."messages" to "authenticated";
grant insert on table "public"."messages" to "authenticated";
grant references on table "public"."messages" to "authenticated";
grant select on table "public"."messages" to "authenticated";
grant trigger on table "public"."messages" to "authenticated";
grant truncate on table "public"."messages" to "authenticated";
grant update on table "public"."messages" to "authenticated";
grant delete on table "public"."messages" to "service_role";
grant insert on table "public"."messages" to "service_role";
grant references on table "public"."messages" to "service_role";
grant select on table "public"."messages" to "service_role";
grant trigger on table "public"."messages" to "service_role";
grant truncate on table "public"."messages" to "service_role";
grant update on table "public"."messages" to "service_role";
create policy "Users can manage their own conversations"
on "public"."conversations"
as permissive
for all
to public
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Users can manage messages in their conversations"
on "public"."messages"
as permissive
for all
to public
using ((( SELECT auth.uid() AS uid) IN ( SELECT conversations.user_id
FROM conversations
WHERE (conversations.id = messages.conversation_id))));
CREATE TRIGGER update_leaf_trigger AFTER INSERT ON public.messages FOR EACH ROW EXECUTE FUNCTION update_conversation_leaf();
create policy "Give users access to own folder images_delete"
on "storage"."objects"
as permissive
for delete
to public
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder images_insert"
on "storage"."objects"
as permissive
for insert
to public
with check (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder images_select"
on "storage"."objects"
as permissive
for select
to public
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder images_update"
on "storage"."objects"
as permissive
for update
to public
using (((bucket_id = 'images'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
@@ -0,0 +1,890 @@
create type "public"."conversation-type" as enum ('parametric', 'creative');
create type "public"."generation-status" as enum ('pending', 'success', 'failure');
create type "public"."mesh_file_type" as enum ('glb', 'stl', 'obj', 'fbx');
create type "public"."mesh_model_type" as enum ('quality', 'fast');
create type "public"."privacy_type" as enum ('public', 'private');
create type "public"."prompt_type" as enum ('mesh', 'image', 'chat');
create type "public"."stripe-level" as enum ('pro', 'standard');
create type "public"."subscription_level" as enum ('pro', 'standard', 'free');
create table "public"."images" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"status" public."generation-status" not null default 'pending'::public."generation-status",
"user_id" uuid not null,
"conversation_id" uuid not null,
"image_generation_call_id" text,
"prompt" jsonb not null default '{}'::jsonb
);
alter table "public"."images" enable row level security;
create table "public"."meshes" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"status" public."generation-status" not null default 'pending'::public."generation-status",
"user_id" uuid not null,
"images" uuid[],
"conversation_id" uuid not null,
"prompt" jsonb not null default '{}'::jsonb,
"file_type" public.mesh_file_type not null default 'glb'::public.mesh_file_type
);
alter table "public"."meshes" enable row level security;
create table "public"."previews" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone not null default now(),
"status" public."generation-status" not null default 'pending'::public."generation-status",
"user_id" uuid not null,
"conversation_id" uuid not null,
"mesh_id" uuid not null
);
alter table "public"."previews" enable row level security;
create table "public"."profiles" (
"id" uuid not null default gen_random_uuid(),
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone not null default now(),
"user_id" uuid not null,
"full_name" text not null,
"notifications_enabled" boolean not null default false,
"avatar_path" text
);
alter table "public"."profiles" enable row level security;
create table "public"."prompts" (
"id" bigint generated by default as identity not null,
"created_at" timestamp with time zone not null default now(),
"user_id" uuid not null,
"type" public.prompt_type not null default 'chat'::public.prompt_type
);
alter table "public"."prompts" enable row level security;
create table "public"."subscriptions" (
"id" uuid not null default gen_random_uuid(),
"user_id" uuid not null,
"stripe_customer_id" text,
"stripe_subscription_id" text,
"status" text,
"created_at" timestamp with time zone default now(),
"level" public."stripe-level" not null default 'pro'::public."stripe-level"
);
alter table "public"."subscriptions" enable row level security;
create table "public"."trial_users" (
"id" uuid not null default gen_random_uuid(),
"user_id" uuid not null
);
alter table "public"."trial_users" enable row level security;
alter table "public"."conversations" add column "privacy" public.privacy_type not null default 'private'::public.privacy_type;
alter table "public"."conversations" add column "settings" jsonb not null default '{}'::jsonb;
alter table "public"."conversations" add column "type" public."conversation-type" not null default 'parametric'::public."conversation-type";
alter table "public"."messages" add column "rating" smallint not null default '0'::smallint;
CREATE INDEX idx_images_image_generation_call_id ON public.images USING btree (image_generation_call_id);
CREATE INDEX idx_subscriptions_stripe_customer_id ON public.subscriptions USING btree (stripe_customer_id);
CREATE INDEX idx_subscriptions_stripe_subscription_id ON public.subscriptions USING btree (stripe_subscription_id);
CREATE INDEX idx_subscriptions_user_id ON public.subscriptions USING btree (user_id);
CREATE UNIQUE INDEX images_pkey ON public.images USING btree (id);
CREATE UNIQUE INDEX meshes_pkey ON public.meshes USING btree (id);
CREATE UNIQUE INDEX previews_pkey ON public.previews USING btree (id);
CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id);
CREATE UNIQUE INDEX prompts_pkey ON public.prompts USING btree (id);
CREATE UNIQUE INDEX subscriptions_pkey ON public.subscriptions USING btree (id);
CREATE UNIQUE INDEX trial_users_pkey ON public.trial_users USING btree (id);
CREATE UNIQUE INDEX trial_users_user_id_key ON public.trial_users USING btree (user_id);
alter table "public"."images" add constraint "images_pkey" PRIMARY KEY using index "images_pkey";
alter table "public"."meshes" add constraint "meshes_pkey" PRIMARY KEY using index "meshes_pkey";
alter table "public"."previews" add constraint "previews_pkey" PRIMARY KEY using index "previews_pkey";
alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey";
alter table "public"."prompts" add constraint "prompts_pkey" PRIMARY KEY using index "prompts_pkey";
alter table "public"."subscriptions" add constraint "subscriptions_pkey" PRIMARY KEY using index "subscriptions_pkey";
alter table "public"."trial_users" add constraint "trial_users_pkey" PRIMARY KEY using index "trial_users_pkey";
alter table "public"."images" add constraint "images_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."images" validate constraint "images_conversation_id_fkey";
alter table "public"."images" add constraint "images_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."images" validate constraint "images_user_id_fkey";
alter table "public"."meshes" add constraint "meshes_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."meshes" validate constraint "meshes_conversation_id_fkey";
alter table "public"."meshes" add constraint "meshes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."meshes" validate constraint "meshes_user_id_fkey";
alter table "public"."previews" add constraint "previews_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES public.conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."previews" validate constraint "previews_conversation_id_fkey";
alter table "public"."previews" add constraint "previews_mesh_id_fkey" FOREIGN KEY (mesh_id) REFERENCES public.meshes(id) ON DELETE CASCADE not valid;
alter table "public"."previews" validate constraint "previews_mesh_id_fkey";
alter table "public"."previews" add constraint "previews_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."previews" validate constraint "previews_user_id_fkey";
alter table "public"."profiles" add constraint "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."profiles" validate constraint "profiles_user_id_fkey";
alter table "public"."prompts" add constraint "prompts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."prompts" validate constraint "prompts_user_id_fkey";
alter table "public"."subscriptions" add constraint "subscriptions_status_check" CHECK ((status = ANY (ARRAY['active'::text, 'canceled'::text, 'incomplete'::text, 'incomplete_expired'::text, 'past_due'::text, 'trialing'::text, 'unpaid'::text]))) not valid;
alter table "public"."subscriptions" validate constraint "subscriptions_status_check";
alter table "public"."subscriptions" add constraint "subscriptions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) not valid;
alter table "public"."subscriptions" validate constraint "subscriptions_user_id_fkey";
alter table "public"."trial_users" add constraint "trial_users_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."trial_users" validate constraint "trial_users_user_id_fkey";
alter table "public"."trial_users" add constraint "trial_users_user_id_key" UNIQUE using index "trial_users_user_id_key";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.handle_mesh_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
-- Insert a prompt entry immediately when a mesh is created
INSERT INTO public.prompts (user_id, type)
VALUES (NEW.user_id, 'mesh');
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.handle_mesh_status_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
-- If mesh status changed to 'failure', remove the most recent mesh prompt for this user
IF OLD.status != 'failure' AND NEW.status = 'failure' THEN
DELETE FROM public.prompts
WHERE user_id = NEW.user_id
AND type = 'mesh'
AND id = (
SELECT id FROM public.prompts
WHERE user_id = NEW.user_id
AND type = 'mesh'
ORDER BY created_at DESC
LIMIT 1
);
END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
BEGIN
INSERT INTO public.profiles (user_id, full_name)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
)
);
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$function$
;
create type "public"."user_data" as ("hasTrialed" boolean, "sublevel" public.subscription_level, "generationsRemaining" integer);
CREATE OR REPLACE FUNCTION public.user_extradata(user_id_input uuid)
RETURNS public.user_data
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
hasTrialed boolean;
userlevel public.subscriptions.level%TYPE;
userstatus public.subscriptions.status%TYPE;
daily_generations_used integer;
monthly_generations_used integer;
ret user_data;
BEGIN
-- Get trial status
SELECT (
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
) INTO hasTrialed;
-- Get subscription info
SELECT STATUS, LEVEL INTO userstatus, userlevel
FROM public.subscriptions
WHERE user_id = user_id_input;
-- Get daily generation count (for free tier)
SELECT COUNT(*) INTO daily_generations_used
FROM public.prompts
WHERE user_id = user_id_input
AND type != 'image'
AND type != 'chat' -- Exclude parametric generations
AND created_at >= NOW() - INTERVAL '1 day';
-- Get monthly generation count (for standard tier)
SELECT COUNT(*) INTO monthly_generations_used
FROM public.prompts
WHERE user_id = user_id_input
AND type != 'image'
AND type != 'chat' -- Exclude parametric generations
AND created_at >= date_trunc('month', NOW());
-- Set return values
ret."hasTrialed" = hasTrialed;
-- Set subscription level
IF (userstatus = 'active') THEN
ret."sublevel" = userlevel;
ELSIF (userstatus = 'trialing') THEN
ret."sublevel" = 'pro';
ELSE
ret."sublevel" = 'free';
END IF;
-- Calculate remaining generations based on subscription level
IF ret."sublevel" = 'free' THEN
-- Free tier: 3 generations per day
ret."generationsRemaining" = GREATEST(3 - daily_generations_used, 0);
ELSIF ret."sublevel" = 'standard' THEN
-- Standard tier: 100 generations per month
ret."generationsRemaining" = GREATEST(100 - monthly_generations_used, 0);
ELSE
-- Pro or enterprise: unlimited
ret."generationsRemaining" = 999999; -- effectively unlimited
END IF;
RETURN ret;
EXCEPTION
WHEN others THEN
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
END;
$function$
;
grant delete on table "public"."images" to "anon";
grant insert on table "public"."images" to "anon";
grant references on table "public"."images" to "anon";
grant select on table "public"."images" to "anon";
grant trigger on table "public"."images" to "anon";
grant truncate on table "public"."images" to "anon";
grant update on table "public"."images" to "anon";
grant delete on table "public"."images" to "authenticated";
grant insert on table "public"."images" to "authenticated";
grant references on table "public"."images" to "authenticated";
grant select on table "public"."images" to "authenticated";
grant trigger on table "public"."images" to "authenticated";
grant truncate on table "public"."images" to "authenticated";
grant update on table "public"."images" to "authenticated";
grant delete on table "public"."images" to "service_role";
grant insert on table "public"."images" to "service_role";
grant references on table "public"."images" to "service_role";
grant select on table "public"."images" to "service_role";
grant trigger on table "public"."images" to "service_role";
grant truncate on table "public"."images" to "service_role";
grant update on table "public"."images" to "service_role";
grant delete on table "public"."meshes" to "anon";
grant insert on table "public"."meshes" to "anon";
grant references on table "public"."meshes" to "anon";
grant select on table "public"."meshes" to "anon";
grant trigger on table "public"."meshes" to "anon";
grant truncate on table "public"."meshes" to "anon";
grant update on table "public"."meshes" to "anon";
grant delete on table "public"."meshes" to "authenticated";
grant insert on table "public"."meshes" to "authenticated";
grant references on table "public"."meshes" to "authenticated";
grant select on table "public"."meshes" to "authenticated";
grant trigger on table "public"."meshes" to "authenticated";
grant truncate on table "public"."meshes" to "authenticated";
grant update on table "public"."meshes" to "authenticated";
grant delete on table "public"."meshes" to "service_role";
grant insert on table "public"."meshes" to "service_role";
grant references on table "public"."meshes" to "service_role";
grant select on table "public"."meshes" to "service_role";
grant trigger on table "public"."meshes" to "service_role";
grant truncate on table "public"."meshes" to "service_role";
grant update on table "public"."meshes" to "service_role";
grant delete on table "public"."previews" to "anon";
grant insert on table "public"."previews" to "anon";
grant references on table "public"."previews" to "anon";
grant select on table "public"."previews" to "anon";
grant trigger on table "public"."previews" to "anon";
grant truncate on table "public"."previews" to "anon";
grant update on table "public"."previews" to "anon";
grant delete on table "public"."previews" to "authenticated";
grant insert on table "public"."previews" to "authenticated";
grant references on table "public"."previews" to "authenticated";
grant select on table "public"."previews" to "authenticated";
grant trigger on table "public"."previews" to "authenticated";
grant truncate on table "public"."previews" to "authenticated";
grant update on table "public"."previews" to "authenticated";
grant delete on table "public"."previews" to "service_role";
grant insert on table "public"."previews" to "service_role";
grant references on table "public"."previews" to "service_role";
grant select on table "public"."previews" to "service_role";
grant trigger on table "public"."previews" to "service_role";
grant truncate on table "public"."previews" to "service_role";
grant update on table "public"."previews" to "service_role";
grant delete on table "public"."profiles" to "anon";
grant insert on table "public"."profiles" to "anon";
grant references on table "public"."profiles" to "anon";
grant select on table "public"."profiles" to "anon";
grant trigger on table "public"."profiles" to "anon";
grant truncate on table "public"."profiles" to "anon";
grant update on table "public"."profiles" to "anon";
grant delete on table "public"."profiles" to "authenticated";
grant insert on table "public"."profiles" to "authenticated";
grant references on table "public"."profiles" to "authenticated";
grant select on table "public"."profiles" to "authenticated";
grant trigger on table "public"."profiles" to "authenticated";
grant truncate on table "public"."profiles" to "authenticated";
grant update on table "public"."profiles" to "authenticated";
grant delete on table "public"."profiles" to "service_role";
grant insert on table "public"."profiles" to "service_role";
grant references on table "public"."profiles" to "service_role";
grant select on table "public"."profiles" to "service_role";
grant trigger on table "public"."profiles" to "service_role";
grant truncate on table "public"."profiles" to "service_role";
grant update on table "public"."profiles" to "service_role";
grant delete on table "public"."prompts" to "anon";
grant insert on table "public"."prompts" to "anon";
grant references on table "public"."prompts" to "anon";
grant select on table "public"."prompts" to "anon";
grant trigger on table "public"."prompts" to "anon";
grant truncate on table "public"."prompts" to "anon";
grant update on table "public"."prompts" to "anon";
grant delete on table "public"."prompts" to "authenticated";
grant insert on table "public"."prompts" to "authenticated";
grant references on table "public"."prompts" to "authenticated";
grant select on table "public"."prompts" to "authenticated";
grant trigger on table "public"."prompts" to "authenticated";
grant truncate on table "public"."prompts" to "authenticated";
grant update on table "public"."prompts" to "authenticated";
grant delete on table "public"."prompts" to "service_role";
grant insert on table "public"."prompts" to "service_role";
grant references on table "public"."prompts" to "service_role";
grant select on table "public"."prompts" to "service_role";
grant trigger on table "public"."prompts" to "service_role";
grant truncate on table "public"."prompts" to "service_role";
grant update on table "public"."prompts" to "service_role";
grant delete on table "public"."subscriptions" to "anon";
grant insert on table "public"."subscriptions" to "anon";
grant references on table "public"."subscriptions" to "anon";
grant select on table "public"."subscriptions" to "anon";
grant trigger on table "public"."subscriptions" to "anon";
grant truncate on table "public"."subscriptions" to "anon";
grant update on table "public"."subscriptions" to "anon";
grant delete on table "public"."subscriptions" to "authenticated";
grant insert on table "public"."subscriptions" to "authenticated";
grant references on table "public"."subscriptions" to "authenticated";
grant select on table "public"."subscriptions" to "authenticated";
grant trigger on table "public"."subscriptions" to "authenticated";
grant truncate on table "public"."subscriptions" to "authenticated";
grant update on table "public"."subscriptions" to "authenticated";
grant delete on table "public"."subscriptions" to "service_role";
grant insert on table "public"."subscriptions" to "service_role";
grant references on table "public"."subscriptions" to "service_role";
grant select on table "public"."subscriptions" to "service_role";
grant trigger on table "public"."subscriptions" to "service_role";
grant truncate on table "public"."subscriptions" to "service_role";
grant update on table "public"."subscriptions" to "service_role";
grant delete on table "public"."trial_users" to "anon";
grant insert on table "public"."trial_users" to "anon";
grant references on table "public"."trial_users" to "anon";
grant select on table "public"."trial_users" to "anon";
grant trigger on table "public"."trial_users" to "anon";
grant truncate on table "public"."trial_users" to "anon";
grant update on table "public"."trial_users" to "anon";
grant delete on table "public"."trial_users" to "authenticated";
grant insert on table "public"."trial_users" to "authenticated";
grant references on table "public"."trial_users" to "authenticated";
grant select on table "public"."trial_users" to "authenticated";
grant trigger on table "public"."trial_users" to "authenticated";
grant truncate on table "public"."trial_users" to "authenticated";
grant update on table "public"."trial_users" to "authenticated";
grant delete on table "public"."trial_users" to "service_role";
grant insert on table "public"."trial_users" to "service_role";
grant references on table "public"."trial_users" to "service_role";
grant select on table "public"."trial_users" to "service_role";
grant trigger on table "public"."trial_users" to "service_role";
grant truncate on table "public"."trial_users" to "service_role";
grant update on table "public"."trial_users" to "service_role";
create policy "Anyone can view a public conversation"
on "public"."conversations"
as permissive
for select
to authenticated, anon
using ((privacy = 'public'::public.privacy_type));
create policy "Public conversations images"
on "public"."images"
as permissive
for select
to authenticated, anon
using ((EXISTS ( SELECT 1
FROM public.conversations
WHERE ((conversations.id = images.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
create policy "User can manage their data"
on "public"."images"
as permissive
for all
to authenticated
using ((( SELECT auth.uid() AS uid) = user_id))
with check ((( SELECT auth.uid() AS uid) = user_id));
create policy "Everyone can view meshes associated with public conversations"
on "public"."meshes"
as permissive
for select
to authenticated, anon
using ((EXISTS ( SELECT 1
FROM public.conversations
WHERE ((conversations.id = meshes.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
create policy "Users can manage their meshes"
on "public"."meshes"
as permissive
for all
to public
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Public conversations messages"
on "public"."messages"
as permissive
for select
to authenticated, anon
using ((EXISTS ( SELECT 1
FROM public.conversations
WHERE ((conversations.id = messages.conversation_id) AND (conversations.privacy = 'public'::public.privacy_type)))));
create policy "Users can manage their own previews"
on "public"."previews"
as permissive
for all
to public
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Users can manage their own profile"
on "public"."profiles"
as permissive
for all
to public
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Users can view their own prompts"
on "public"."prompts"
as permissive
for select
to public
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Service role can manage all subscriptions"
on "public"."subscriptions"
as permissive
for all
to service_role
using (true)
with check (true);
create policy "Users can read their own subscriptions"
on "public"."subscriptions"
as permissive
for select
to authenticated
using ((( SELECT auth.uid() AS uid) = user_id));
create policy "Enable users to view their own data only"
on "public"."trial_users"
as permissive
for select
to authenticated
using ((( SELECT auth.uid() AS uid) = user_id));
CREATE TRIGGER mesh_insert_prompt_trigger AFTER INSERT ON public.meshes FOR EACH ROW EXECUTE FUNCTION public.handle_mesh_insert();
CREATE TRIGGER mesh_status_update_trigger AFTER UPDATE ON public.meshes FOR EACH ROW WHEN ((old.status IS DISTINCT FROM new.status)) EXECUTE FUNCTION public.handle_mesh_status_update();
CREATE TRIGGER update_previews_updated_at BEFORE UPDATE ON public.previews FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
create policy "Allow public read access to temp multiview images"
on "storage"."objects"
as permissive
for select
to public
using ((bucket_id = 'temp-multiview'::text));
create policy "Allow service role to delete temp multiview images"
on "storage"."objects"
as permissive
for delete
to service_role
using ((bucket_id = 'temp-multiview'::text));
create policy "Allow service role to upload temp multiview images"
on "storage"."objects"
as permissive
for insert
to service_role
with check ((bucket_id = 'temp-multiview'::text));
create policy "Give users access to own folder meshes_delete"
on "storage"."objects"
as permissive
for delete
to public
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder meshes_insert"
on "storage"."objects"
as permissive
for insert
to public
with check (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder meshes_select"
on "storage"."objects"
as permissive
for select
to public
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder meshes_update"
on "storage"."objects"
as permissive
for update
to public
using (((bucket_id = 'meshes'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder previews_delete"
on "storage"."objects"
as permissive
for delete
to public
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder previews_insert"
on "storage"."objects"
as permissive
for insert
to public
with check (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder previews_select"
on "storage"."objects"
as permissive
for select
to public
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Give users access to own folder previews_update"
on "storage"."objects"
as permissive
for update
to public
using (((bucket_id = 'previews'::text) AND (( SELECT (auth.uid())::text AS uid) = (storage.foldername(name))[1])));
create policy "Public conversations allow anyone to view images_select"
on "storage"."objects"
as permissive
for select
to anon, authenticated
using (((bucket_id = 'images'::text) AND (EXISTS ( SELECT 1
FROM public.conversations
WHERE ((conversations.privacy = 'public'::public.privacy_type) AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
create policy "Public conversations allow anyone to view meshes_select"
on "storage"."objects"
as permissive
for select
to anon, authenticated
using (((bucket_id = 'meshes'::text) AND (EXISTS ( SELECT 1
FROM public.conversations
WHERE ((conversations.privacy = 'public'::public.privacy_type) AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
@@ -0,0 +1,723 @@
create extension if not exists "pg_cron" with schema "pg_catalog";
create type "public"."token_operation_type" as enum ('mesh', 'parametric', 'chat', 'refund');
create type "public"."token_source_type" as enum ('subscription', 'purchased');
drop trigger if exists "mesh_insert_prompt_trigger" on "public"."meshes";
drop function if exists "public"."handle_mesh_insert"();
drop function if exists "public"."user_extradata"(uuid);
drop type "public"."user_data";
create table "public"."token_balances" (
"id" uuid not null default gen_random_uuid(),
"user_id" uuid not null,
"source" public.token_source_type not null,
"balance" integer not null default 0,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone not null default now()
);
alter table "public"."token_balances" enable row level security;
create table "public"."token_costs" (
"operation" public.token_operation_type not null,
"cost" integer not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone not null default now()
);
alter table "public"."token_costs" enable row level security;
create table "public"."token_pack_products" (
"id" uuid not null default gen_random_uuid(),
"stripe_lookup_key" text not null,
"token_amount" integer not null,
"name" text not null,
"price_cents" integer not null,
"active" boolean not null default true,
"created_at" timestamp with time zone not null default now()
);
-- Default costs
INSERT INTO "public"."token_costs" ("operation", "cost") VALUES
('mesh', 30),
('parametric', 5),
('chat', 1);
alter table "public"."token_pack_products" enable row level security;
create table "public"."token_transactions" (
"id" bigint generated always as identity not null,
"user_id" uuid not null,
"operation" public.token_operation_type not null,
"amount" integer not null,
"source" public.token_source_type not null,
"reference_id" text,
"subscription_balance_after" integer not null,
"purchased_balance_after" integer not null,
"created_at" timestamp with time zone not null default now()
);
alter table "public"."token_transactions" enable row level security;
CREATE UNIQUE INDEX token_balances_pkey ON public.token_balances USING btree (id);
CREATE UNIQUE INDEX token_balances_user_id_source_key ON public.token_balances USING btree (user_id, source);
CREATE UNIQUE INDEX token_costs_pkey ON public.token_costs USING btree (operation);
CREATE UNIQUE INDEX token_pack_products_pkey ON public.token_pack_products USING btree (id);
CREATE UNIQUE INDEX token_pack_products_stripe_lookup_key_key ON public.token_pack_products USING btree (stripe_lookup_key);
CREATE UNIQUE INDEX token_transactions_pkey ON public.token_transactions USING btree (id);
alter table "public"."token_balances" add constraint "token_balances_pkey" PRIMARY KEY using index "token_balances_pkey";
alter table "public"."token_costs" add constraint "token_costs_pkey" PRIMARY KEY using index "token_costs_pkey";
alter table "public"."token_pack_products" add constraint "token_pack_products_pkey" PRIMARY KEY using index "token_pack_products_pkey";
alter table "public"."token_transactions" add constraint "token_transactions_pkey" PRIMARY KEY using index "token_transactions_pkey";
alter table "public"."token_balances" add constraint "token_balances_balance_check" CHECK ((balance >= 0)) not valid;
alter table "public"."token_balances" validate constraint "token_balances_balance_check";
alter table "public"."token_balances" add constraint "token_balances_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
alter table "public"."token_balances" validate constraint "token_balances_user_id_fkey";
alter table "public"."token_balances" add constraint "token_balances_user_id_source_key" UNIQUE using index "token_balances_user_id_source_key";
alter table "public"."token_pack_products" add constraint "token_pack_products_stripe_lookup_key_key" UNIQUE using index "token_pack_products_stripe_lookup_key_key";
alter table "public"."token_transactions" add constraint "token_transactions_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
alter table "public"."token_transactions" validate constraint "token_transactions_user_id_fkey";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.credit_purchased_tokens(p_user_id uuid, p_amount integer, p_reference_id text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_new_balance integer;
v_sub_balance integer;
BEGIN
INSERT INTO public.token_balances (user_id, source, balance)
VALUES (p_user_id, 'purchased'::public.token_source_type, p_amount)
ON CONFLICT (user_id, source) DO UPDATE
SET balance = token_balances.balance + p_amount, updated_at = now()
RETURNING balance INTO v_new_balance;
SELECT COALESCE(balance, 0) INTO v_sub_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'subscription'::public.token_source_type;
v_sub_balance := COALESCE(v_sub_balance, 0);
INSERT INTO public.token_transactions (
user_id, operation, amount, source, reference_id,
subscription_balance_after, purchased_balance_after
) VALUES (
p_user_id, 'chat'::public.token_operation_type, p_amount, 'purchased'::public.token_source_type, p_reference_id,
v_sub_balance, v_new_balance
);
RETURN jsonb_build_object(
'success', true,
'tokensAdded', p_amount,
'purchasedBalance', v_new_balance
);
END;
$function$
;
CREATE OR REPLACE FUNCTION public.deduct_tokens(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_cost integer;
v_sub_balance integer;
v_pur_balance integer;
v_sub_expires timestamptz;
v_remaining integer;
v_sub_deduct integer;
v_pur_deduct integer;
BEGIN
-- Get cost for operation
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
IF v_cost IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
END IF;
-- Lock and read subscription balance
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'subscription'
FOR UPDATE;
-- If subscription tokens have expired, treat as 0
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
v_sub_balance := 0;
END IF;
-- Lock and read purchased balance
SELECT balance INTO v_pur_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'purchased'
FOR UPDATE;
-- Default to 0 if no rows exist
v_sub_balance := COALESCE(v_sub_balance, 0);
v_pur_balance := COALESCE(v_pur_balance, 0);
-- Check total available
IF (v_sub_balance + v_pur_balance) < v_cost THEN
RETURN jsonb_build_object(
'success', false,
'error', 'insufficient_tokens',
'tokensRequired', v_cost,
'tokensAvailable', v_sub_balance + v_pur_balance
);
END IF;
-- Consume subscription tokens first (they expire anyway)
v_sub_deduct := LEAST(v_cost, v_sub_balance);
v_pur_deduct := v_cost - v_sub_deduct;
-- Update subscription balance
IF v_sub_deduct > 0 THEN
UPDATE public.token_balances
SET balance = balance - v_sub_deduct, updated_at = now()
WHERE user_id = p_user_id AND source = 'subscription';
END IF;
-- Update purchased balance
IF v_pur_deduct > 0 THEN
UPDATE public.token_balances
SET balance = balance - v_pur_deduct, updated_at = now()
WHERE user_id = p_user_id AND source = 'purchased';
END IF;
-- Record transaction
INSERT INTO public.token_transactions (
user_id, operation, amount, source, reference_id,
subscription_balance_after, purchased_balance_after
) VALUES (
p_user_id, p_operation, -v_cost,
CASE WHEN v_sub_deduct > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
p_reference_id,
v_sub_balance - v_sub_deduct,
v_pur_balance - v_pur_deduct
);
RETURN jsonb_build_object(
'success', true,
'tokensDeducted', v_cost,
'subscriptionBalance', v_sub_balance - v_sub_deduct,
'purchasedBalance', v_pur_balance - v_pur_deduct,
'totalBalance', (v_sub_balance - v_sub_deduct) + (v_pur_balance - v_pur_deduct)
);
END;
$function$
;
CREATE OR REPLACE FUNCTION public.get_subscription_token_limit(p_user_id uuid)
RETURNS integer
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
userlevel public.subscriptions.level%TYPE;
userstatus public.subscriptions.status%TYPE;
BEGIN
SELECT status, level INTO userstatus, userlevel
FROM public.subscriptions
WHERE user_id = p_user_id;
IF userstatus = 'active' OR userstatus = 'trialing' THEN
IF userlevel = 'pro' THEN
RETURN 5000;
ELSIF userlevel = 'standard' THEN
RETURN 1000;
END IF;
END IF;
-- Free tier
RETURN 50;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.grant_subscription_tokens(p_user_id uuid, p_token_amount integer, p_expires_at timestamp with time zone)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_pur_balance integer;
BEGIN
INSERT INTO public.token_balances (user_id, source, balance, expires_at)
VALUES (p_user_id, 'subscription'::public.token_source_type, p_token_amount, p_expires_at)
ON CONFLICT (user_id, source) DO UPDATE
SET balance = p_token_amount, expires_at = p_expires_at, updated_at = now();
SELECT COALESCE(balance, 0) INTO v_pur_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'purchased'::public.token_source_type;
v_pur_balance := COALESCE(v_pur_balance, 0);
INSERT INTO public.token_transactions (
user_id, operation, amount, source, reference_id,
subscription_balance_after, purchased_balance_after
) VALUES (
p_user_id, 'chat'::public.token_operation_type, p_token_amount, 'subscription'::public.token_source_type, 'subscription_grant',
p_token_amount, v_pur_balance
);
RETURN jsonb_build_object(
'success', true,
'tokensGranted', p_token_amount,
'subscriptionBalance', p_token_amount,
'expiresAt', p_expires_at
);
END;
$function$
;
CREATE OR REPLACE FUNCTION public.refund_tokens(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_cost integer;
v_sub_balance integer;
v_pur_balance integer;
v_sub_limit integer;
v_sub_refund integer;
v_pur_refund integer;
BEGIN
-- Get cost for operation
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
IF v_cost IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
END IF;
-- Get current balances with lock
SELECT balance INTO v_sub_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'subscription'
FOR UPDATE;
SELECT balance INTO v_pur_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'purchased'
FOR UPDATE;
v_sub_balance := COALESCE(v_sub_balance, 0);
v_pur_balance := COALESCE(v_pur_balance, 0);
-- Get subscription tier limit
v_sub_limit := public.get_subscription_token_limit(p_user_id);
-- Refund to subscription first (up to tier limit), remainder to purchased
v_sub_refund := LEAST(v_cost, v_sub_limit - v_sub_balance);
v_sub_refund := GREATEST(v_sub_refund, 0);
v_pur_refund := v_cost - v_sub_refund;
-- Update balances
IF v_sub_refund > 0 THEN
UPDATE public.token_balances
SET balance = balance + v_sub_refund, updated_at = now()
WHERE user_id = p_user_id AND source = 'subscription';
END IF;
IF v_pur_refund > 0 THEN
UPDATE public.token_balances
SET balance = balance + v_pur_refund, updated_at = now()
WHERE user_id = p_user_id AND source = 'purchased';
END IF;
-- Record refund transaction
INSERT INTO public.token_transactions (
user_id, operation, amount, source, reference_id,
subscription_balance_after, purchased_balance_after
) VALUES (
p_user_id, 'refund'::public.token_operation_type, v_cost,
CASE WHEN v_sub_refund > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
p_reference_id,
v_sub_balance + v_sub_refund,
v_pur_balance + v_pur_refund
);
RETURN jsonb_build_object(
'success', true,
'tokensRefunded', v_cost,
'subscriptionBalance', v_sub_balance + v_sub_refund,
'purchasedBalance', v_pur_balance + v_pur_refund
);
END;
$function$
;
CREATE OR REPLACE FUNCTION public.reset_free_tier_tokens()
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
UPDATE public.token_balances tb
SET balance = 50,
expires_at = now() + interval '1 day',
updated_at = now()
WHERE tb.source = 'subscription'
AND NOT EXISTS (
SELECT 1 FROM public.subscriptions s
WHERE s.user_id = tb.user_id
AND s.status IN ('active', 'trialing')
)
AND (tb.expires_at IS NULL OR tb.expires_at < now());
END;
$function$
;
CREATE OR REPLACE FUNCTION public.handle_mesh_status_update()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
BEGIN
-- If mesh status changed to 'failure', refund the tokens
IF OLD.status != 'failure' AND NEW.status = 'failure' THEN
PERFORM public.refund_tokens(NEW.user_id, 'mesh'::public.token_operation_type, NEW.id::text);
END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
BEGIN
INSERT INTO public.profiles (user_id, full_name)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
)
);
-- Initialize subscription token balance (free tier: 50 tokens, 1-day expiry)
INSERT INTO public.token_balances (user_id, source, balance, expires_at)
VALUES (NEW.id, 'subscription'::public.token_source_type, 50, now() + interval '1 day');
-- Initialize purchased token balance (0)
INSERT INTO public.token_balances (user_id, source, balance)
VALUES (NEW.id, 'purchased'::public.token_source_type, 0);
RETURN NEW;
END;
$function$
;
create type "public"."user_data" as ("hasTrialed" boolean, "sublevel" public.subscription_level, "subscriptionTokens" integer, "purchasedTokens" integer, "totalTokens" integer, "subscriptionTokenLimit" integer, "subscriptionExpiresAt" timestamp with time zone);
CREATE OR REPLACE FUNCTION public.user_extradata(user_id_input uuid)
RETURNS public.user_data
LANGUAGE plpgsql
STABLE
AS $function$
DECLARE
hasTrialed boolean;
userlevel public.subscriptions.level%TYPE;
userstatus public.subscriptions.status%TYPE;
v_sub_balance integer;
v_pur_balance integer;
v_sub_expires timestamptz;
v_sub_limit integer;
ret user_data;
BEGIN
-- Get trial status
SELECT (
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
) INTO hasTrialed;
-- Get subscription info
SELECT STATUS, LEVEL INTO userstatus, userlevel
FROM public.subscriptions
WHERE user_id = user_id_input;
-- Get token balances
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
FROM public.token_balances
WHERE user_id = user_id_input AND source = 'subscription';
SELECT balance INTO v_pur_balance
FROM public.token_balances
WHERE user_id = user_id_input AND source = 'purchased';
v_sub_balance := COALESCE(v_sub_balance, 0);
v_pur_balance := COALESCE(v_pur_balance, 0);
-- If subscription tokens have expired, treat as 0
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
v_sub_balance := 0;
END IF;
-- Set return values
ret."hasTrialed" = hasTrialed;
-- Set subscription level
IF (userstatus = 'active') THEN
ret."sublevel" = userlevel;
ELSIF (userstatus = 'trialing') THEN
ret."sublevel" = 'pro';
ELSE
ret."sublevel" = 'free';
END IF;
-- Get token limit for tier
v_sub_limit := public.get_subscription_token_limit(user_id_input);
-- Set token values
ret."subscriptionTokens" = v_sub_balance;
ret."purchasedTokens" = v_pur_balance;
ret."totalTokens" = v_sub_balance + v_pur_balance;
ret."subscriptionTokenLimit" = v_sub_limit;
ret."subscriptionExpiresAt" = v_sub_expires;
RETURN ret;
EXCEPTION
WHEN others THEN
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
END;
$function$
;
grant delete on table "public"."token_balances" to "anon";
grant insert on table "public"."token_balances" to "anon";
grant references on table "public"."token_balances" to "anon";
grant select on table "public"."token_balances" to "anon";
grant trigger on table "public"."token_balances" to "anon";
grant truncate on table "public"."token_balances" to "anon";
grant update on table "public"."token_balances" to "anon";
grant delete on table "public"."token_balances" to "authenticated";
grant insert on table "public"."token_balances" to "authenticated";
grant references on table "public"."token_balances" to "authenticated";
grant select on table "public"."token_balances" to "authenticated";
grant trigger on table "public"."token_balances" to "authenticated";
grant truncate on table "public"."token_balances" to "authenticated";
grant update on table "public"."token_balances" to "authenticated";
grant delete on table "public"."token_balances" to "service_role";
grant insert on table "public"."token_balances" to "service_role";
grant references on table "public"."token_balances" to "service_role";
grant select on table "public"."token_balances" to "service_role";
grant trigger on table "public"."token_balances" to "service_role";
grant truncate on table "public"."token_balances" to "service_role";
grant update on table "public"."token_balances" to "service_role";
grant delete on table "public"."token_costs" to "anon";
grant insert on table "public"."token_costs" to "anon";
grant references on table "public"."token_costs" to "anon";
grant select on table "public"."token_costs" to "anon";
grant trigger on table "public"."token_costs" to "anon";
grant truncate on table "public"."token_costs" to "anon";
grant update on table "public"."token_costs" to "anon";
grant delete on table "public"."token_costs" to "authenticated";
grant insert on table "public"."token_costs" to "authenticated";
grant references on table "public"."token_costs" to "authenticated";
grant select on table "public"."token_costs" to "authenticated";
grant trigger on table "public"."token_costs" to "authenticated";
grant truncate on table "public"."token_costs" to "authenticated";
grant update on table "public"."token_costs" to "authenticated";
grant delete on table "public"."token_costs" to "service_role";
grant insert on table "public"."token_costs" to "service_role";
grant references on table "public"."token_costs" to "service_role";
grant select on table "public"."token_costs" to "service_role";
grant trigger on table "public"."token_costs" to "service_role";
grant truncate on table "public"."token_costs" to "service_role";
grant update on table "public"."token_costs" to "service_role";
grant delete on table "public"."token_pack_products" to "anon";
grant insert on table "public"."token_pack_products" to "anon";
grant references on table "public"."token_pack_products" to "anon";
grant select on table "public"."token_pack_products" to "anon";
grant trigger on table "public"."token_pack_products" to "anon";
grant truncate on table "public"."token_pack_products" to "anon";
grant update on table "public"."token_pack_products" to "anon";
grant delete on table "public"."token_pack_products" to "authenticated";
grant insert on table "public"."token_pack_products" to "authenticated";
grant references on table "public"."token_pack_products" to "authenticated";
grant select on table "public"."token_pack_products" to "authenticated";
grant trigger on table "public"."token_pack_products" to "authenticated";
grant truncate on table "public"."token_pack_products" to "authenticated";
grant update on table "public"."token_pack_products" to "authenticated";
grant delete on table "public"."token_pack_products" to "service_role";
grant insert on table "public"."token_pack_products" to "service_role";
grant references on table "public"."token_pack_products" to "service_role";
grant select on table "public"."token_pack_products" to "service_role";
grant trigger on table "public"."token_pack_products" to "service_role";
grant truncate on table "public"."token_pack_products" to "service_role";
grant update on table "public"."token_pack_products" to "service_role";
grant delete on table "public"."token_transactions" to "anon";
grant insert on table "public"."token_transactions" to "anon";
grant references on table "public"."token_transactions" to "anon";
grant select on table "public"."token_transactions" to "anon";
grant trigger on table "public"."token_transactions" to "anon";
grant truncate on table "public"."token_transactions" to "anon";
grant update on table "public"."token_transactions" to "anon";
grant delete on table "public"."token_transactions" to "authenticated";
grant insert on table "public"."token_transactions" to "authenticated";
grant references on table "public"."token_transactions" to "authenticated";
grant select on table "public"."token_transactions" to "authenticated";
grant trigger on table "public"."token_transactions" to "authenticated";
grant truncate on table "public"."token_transactions" to "authenticated";
grant update on table "public"."token_transactions" to "authenticated";
grant delete on table "public"."token_transactions" to "service_role";
grant insert on table "public"."token_transactions" to "service_role";
grant references on table "public"."token_transactions" to "service_role";
grant select on table "public"."token_transactions" to "service_role";
grant trigger on table "public"."token_transactions" to "service_role";
grant truncate on table "public"."token_transactions" to "service_role";
grant update on table "public"."token_transactions" to "service_role";
create policy "token_balances_read_own"
on "public"."token_balances"
as permissive
for select
to authenticated
using ((auth.uid() = user_id));
create policy "token_costs_read"
on "public"."token_costs"
as permissive
for select
to authenticated
using (true);
create policy "token_pack_products_read"
on "public"."token_pack_products"
as permissive
for select
to authenticated
using ((active = true));
create policy "token_transactions_read_own"
on "public"."token_transactions"
as permissive
for select
to authenticated
using ((auth.uid() = user_id));
@@ -0,0 +1,243 @@
-- Fix free-tier daily token reset.
--
-- Bug 1: reset_free_tier_tokens() set expires_at = now() + 1 day. Because pg_cron
-- fires with small execution jitter, each day's stored expires_at crept
-- slightly ahead of the next cron's now(), and the gate
-- "expires_at < now()" made the cron skip its own prior resets.
--
-- Bug 2: user_extradata() and deduct_tokens() zero the displayed balance the
-- instant expires_at < now(). So any window between expiry and the next
-- (possibly skipped) cron caused users to see "You've used all your tokens"
-- with their real balance still sitting unchanged in the DB.
--
-- Fix: add a just-in-time reset on every read/deduct. The cron stays as a
-- backstop and is rewritten to pin expires_at to the next UTC day boundary so
-- it no longer drifts or skips.
CREATE OR REPLACE FUNCTION "public"."ensure_free_tier_fresh"("p_user_id" uuid)
RETURNS void
LANGUAGE "plpgsql"
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_is_free boolean;
v_caller uuid;
BEGIN
-- Only allow service role (server-side edge functions) or the user acting
-- on themselves. auth.role() = 'anon' with uid = NULL would otherwise slip
-- past a uid-only check. Silent return, not an exception, so that a
-- legitimate caller's outer flow (user_extradata, deduct_tokens) continues
-- on its own fallback path rather than erroring to the client.
v_caller := auth.uid();
IF auth.role() IS DISTINCT FROM 'service_role' AND v_caller IS DISTINCT FROM p_user_id THEN
RETURN;
END IF;
SELECT NOT EXISTS (
SELECT 1 FROM public.subscriptions
WHERE user_id = p_user_id
AND status IN ('active', 'trialing')
) INTO v_is_free;
IF NOT v_is_free THEN
RETURN;
END IF;
INSERT INTO public.token_balances (user_id, source, balance, expires_at, updated_at)
VALUES (
p_user_id,
'subscription',
50,
date_trunc('day', now()) + interval '1 day',
now()
)
ON CONFLICT (user_id, source) DO UPDATE
SET balance = 50,
expires_at = date_trunc('day', now()) + interval '1 day',
updated_at = now()
WHERE token_balances.expires_at IS NULL
OR token_balances.expires_at <= now();
END;
$$;
CREATE OR REPLACE FUNCTION "public"."deduct_tokens"(
"p_user_id" uuid,
"p_operation" "public"."token_operation_type",
"p_reference_id" text DEFAULT NULL
)
RETURNS jsonb
LANGUAGE "plpgsql"
AS $$
DECLARE
v_cost integer;
v_sub_balance integer;
v_pur_balance integer;
v_sub_expires timestamptz;
v_remaining integer;
v_sub_deduct integer;
v_pur_deduct integer;
BEGIN
SELECT cost INTO v_cost FROM public.token_costs WHERE operation = p_operation;
IF v_cost IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'unknown_operation');
END IF;
PERFORM public.ensure_free_tier_fresh(p_user_id);
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'subscription'
FOR UPDATE;
-- Expired subscription tokens count as 0. For free-tier users this is
-- unreachable after ensure_free_tier_fresh (expires_at is always future).
-- It is load-bearing for paid users whose billing-cycle sub has lapsed,
-- and for the edge case where ensure_free_tier_fresh silently bailed
-- (auth-mismatch). Don't remove without preserving both cases.
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
v_sub_balance := 0;
END IF;
SELECT balance INTO v_pur_balance
FROM public.token_balances
WHERE user_id = p_user_id AND source = 'purchased'
FOR UPDATE;
v_sub_balance := COALESCE(v_sub_balance, 0);
v_pur_balance := COALESCE(v_pur_balance, 0);
IF (v_sub_balance + v_pur_balance) < v_cost THEN
RETURN jsonb_build_object(
'success', false,
'error', 'insufficient_tokens',
'tokensRequired', v_cost,
'tokensAvailable', v_sub_balance + v_pur_balance
);
END IF;
v_sub_deduct := LEAST(v_cost, v_sub_balance);
v_pur_deduct := v_cost - v_sub_deduct;
IF v_sub_deduct > 0 THEN
UPDATE public.token_balances
SET balance = balance - v_sub_deduct, updated_at = now()
WHERE user_id = p_user_id AND source = 'subscription';
END IF;
IF v_pur_deduct > 0 THEN
UPDATE public.token_balances
SET balance = balance - v_pur_deduct, updated_at = now()
WHERE user_id = p_user_id AND source = 'purchased';
END IF;
INSERT INTO public.token_transactions (
user_id, operation, amount, source, reference_id,
subscription_balance_after, purchased_balance_after
) VALUES (
p_user_id, p_operation, -v_cost,
CASE WHEN v_sub_deduct > 0 THEN 'subscription'::public.token_source_type ELSE 'purchased'::public.token_source_type END,
p_reference_id,
v_sub_balance - v_sub_deduct,
v_pur_balance - v_pur_deduct
);
RETURN jsonb_build_object(
'success', true,
'tokensDeducted', v_cost,
'subscriptionBalance', v_sub_balance - v_sub_deduct,
'purchasedBalance', v_pur_balance - v_pur_deduct,
'totalBalance', (v_sub_balance - v_sub_deduct) + (v_pur_balance - v_pur_deduct)
);
END;
$$;
CREATE OR REPLACE FUNCTION "public"."reset_free_tier_tokens"()
RETURNS void
LANGUAGE "plpgsql"
AS $$
BEGIN
UPDATE public.token_balances tb
SET balance = 50,
expires_at = date_trunc('day', now()) + interval '1 day',
updated_at = now()
WHERE tb.source = 'subscription'
AND NOT EXISTS (
SELECT 1 FROM public.subscriptions s
WHERE s.user_id = tb.user_id
AND s.status IN ('active', 'trialing')
)
-- Idempotency guard: don't re-credit users whose period is still live.
-- With expires_at pinned to the day boundary, the scheduled midnight run
-- satisfies `<=` exactly; manual or retried mid-day runs are safely no-ops.
AND (tb.expires_at IS NULL OR tb.expires_at <= now());
END;
$$;
CREATE OR REPLACE FUNCTION "public"."user_extradata"("user_id_input" "uuid")
RETURNS "public"."user_data"
LANGUAGE "plpgsql"
AS $$
DECLARE
hasTrialed boolean;
userlevel public.subscriptions.level%TYPE;
userstatus public.subscriptions.status%TYPE;
v_sub_balance integer;
v_pur_balance integer;
v_sub_expires timestamptz;
v_sub_limit integer;
ret user_data;
BEGIN
PERFORM public.ensure_free_tier_fresh(user_id_input);
SELECT (
(SELECT count(*) FROM public.trial_users WHERE user_id = user_id_input) > 0
) INTO hasTrialed;
SELECT STATUS, LEVEL INTO userstatus, userlevel
FROM public.subscriptions
WHERE user_id = user_id_input;
SELECT balance, expires_at INTO v_sub_balance, v_sub_expires
FROM public.token_balances
WHERE user_id = user_id_input AND source = 'subscription';
SELECT balance INTO v_pur_balance
FROM public.token_balances
WHERE user_id = user_id_input AND source = 'purchased';
v_sub_balance := COALESCE(v_sub_balance, 0);
v_pur_balance := COALESCE(v_pur_balance, 0);
-- See matching comment in deduct_tokens: this branch is load-bearing for
-- paid users whose billing-cycle sub has lapsed and as a fallback if
-- ensure_free_tier_fresh silently bailed. Do not remove.
IF v_sub_expires IS NOT NULL AND v_sub_expires < now() THEN
v_sub_balance := 0;
END IF;
ret."hasTrialed" = hasTrialed;
IF (userstatus = 'active') THEN
ret."sublevel" = userlevel;
ELSIF (userstatus = 'trialing') THEN
ret."sublevel" = 'pro';
ELSE
ret."sublevel" = 'free';
END IF;
v_sub_limit := public.get_subscription_token_limit(user_id_input);
ret."subscriptionTokens" = v_sub_balance;
ret."purchasedTokens" = v_pur_balance;
ret."totalTokens" = v_sub_balance + v_pur_balance;
ret."subscriptionTokenLimit" = v_sub_limit;
ret."subscriptionExpiresAt" = v_sub_expires;
RETURN ret;
EXCEPTION
WHEN others THEN
RAISE EXCEPTION 'An error occurred in function user_extradata(): %', SQLERRM;
END;
$$;
@@ -0,0 +1,14 @@
-- Unschedule the free-tier cron. adam-billing now owns free-tier refreshes
-- (it runs its own /v1/cron/refresh-free-tier against its own DB), so CADAM's
-- reset_free_tier_tokens() no longer needs to fire. The function itself is
-- preserved for now as part of the deferred-drop plan; a follow-up migration
-- will drop the billing tables/functions after adam-billing is stable.
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM cron.job WHERE jobname = 'reset-free-tier-tokens'
) THEN
PERFORM cron.unschedule('reset-free-tier-tokens');
END IF;
END;
$$;
@@ -0,0 +1,385 @@
drop trigger if exists "mesh_status_update_trigger" on "public"."meshes";
drop policy "Service role can manage all subscriptions" on "public"."subscriptions";
drop policy "Users can read their own subscriptions" on "public"."subscriptions";
drop policy "token_balances_read_own" on "public"."token_balances";
drop policy "token_costs_read" on "public"."token_costs";
drop policy "token_pack_products_read" on "public"."token_pack_products";
drop policy "token_transactions_read_own" on "public"."token_transactions";
drop policy "Enable users to view their own data only" on "public"."trial_users";
revoke delete on table "public"."subscriptions" from "anon";
revoke insert on table "public"."subscriptions" from "anon";
revoke references on table "public"."subscriptions" from "anon";
revoke select on table "public"."subscriptions" from "anon";
revoke trigger on table "public"."subscriptions" from "anon";
revoke truncate on table "public"."subscriptions" from "anon";
revoke update on table "public"."subscriptions" from "anon";
revoke delete on table "public"."subscriptions" from "authenticated";
revoke insert on table "public"."subscriptions" from "authenticated";
revoke references on table "public"."subscriptions" from "authenticated";
revoke select on table "public"."subscriptions" from "authenticated";
revoke trigger on table "public"."subscriptions" from "authenticated";
revoke truncate on table "public"."subscriptions" from "authenticated";
revoke update on table "public"."subscriptions" from "authenticated";
revoke delete on table "public"."subscriptions" from "service_role";
revoke insert on table "public"."subscriptions" from "service_role";
revoke references on table "public"."subscriptions" from "service_role";
revoke select on table "public"."subscriptions" from "service_role";
revoke trigger on table "public"."subscriptions" from "service_role";
revoke truncate on table "public"."subscriptions" from "service_role";
revoke update on table "public"."subscriptions" from "service_role";
revoke delete on table "public"."token_balances" from "anon";
revoke insert on table "public"."token_balances" from "anon";
revoke references on table "public"."token_balances" from "anon";
revoke select on table "public"."token_balances" from "anon";
revoke trigger on table "public"."token_balances" from "anon";
revoke truncate on table "public"."token_balances" from "anon";
revoke update on table "public"."token_balances" from "anon";
revoke delete on table "public"."token_balances" from "authenticated";
revoke insert on table "public"."token_balances" from "authenticated";
revoke references on table "public"."token_balances" from "authenticated";
revoke select on table "public"."token_balances" from "authenticated";
revoke trigger on table "public"."token_balances" from "authenticated";
revoke truncate on table "public"."token_balances" from "authenticated";
revoke update on table "public"."token_balances" from "authenticated";
revoke delete on table "public"."token_balances" from "service_role";
revoke insert on table "public"."token_balances" from "service_role";
revoke references on table "public"."token_balances" from "service_role";
revoke select on table "public"."token_balances" from "service_role";
revoke trigger on table "public"."token_balances" from "service_role";
revoke truncate on table "public"."token_balances" from "service_role";
revoke update on table "public"."token_balances" from "service_role";
revoke delete on table "public"."token_costs" from "anon";
revoke insert on table "public"."token_costs" from "anon";
revoke references on table "public"."token_costs" from "anon";
revoke select on table "public"."token_costs" from "anon";
revoke trigger on table "public"."token_costs" from "anon";
revoke truncate on table "public"."token_costs" from "anon";
revoke update on table "public"."token_costs" from "anon";
revoke delete on table "public"."token_costs" from "authenticated";
revoke insert on table "public"."token_costs" from "authenticated";
revoke references on table "public"."token_costs" from "authenticated";
revoke select on table "public"."token_costs" from "authenticated";
revoke trigger on table "public"."token_costs" from "authenticated";
revoke truncate on table "public"."token_costs" from "authenticated";
revoke update on table "public"."token_costs" from "authenticated";
revoke delete on table "public"."token_costs" from "service_role";
revoke insert on table "public"."token_costs" from "service_role";
revoke references on table "public"."token_costs" from "service_role";
revoke select on table "public"."token_costs" from "service_role";
revoke trigger on table "public"."token_costs" from "service_role";
revoke truncate on table "public"."token_costs" from "service_role";
revoke update on table "public"."token_costs" from "service_role";
revoke delete on table "public"."token_pack_products" from "anon";
revoke insert on table "public"."token_pack_products" from "anon";
revoke references on table "public"."token_pack_products" from "anon";
revoke select on table "public"."token_pack_products" from "anon";
revoke trigger on table "public"."token_pack_products" from "anon";
revoke truncate on table "public"."token_pack_products" from "anon";
revoke update on table "public"."token_pack_products" from "anon";
revoke delete on table "public"."token_pack_products" from "authenticated";
revoke insert on table "public"."token_pack_products" from "authenticated";
revoke references on table "public"."token_pack_products" from "authenticated";
revoke select on table "public"."token_pack_products" from "authenticated";
revoke trigger on table "public"."token_pack_products" from "authenticated";
revoke truncate on table "public"."token_pack_products" from "authenticated";
revoke update on table "public"."token_pack_products" from "authenticated";
revoke delete on table "public"."token_pack_products" from "service_role";
revoke insert on table "public"."token_pack_products" from "service_role";
revoke references on table "public"."token_pack_products" from "service_role";
revoke select on table "public"."token_pack_products" from "service_role";
revoke trigger on table "public"."token_pack_products" from "service_role";
revoke truncate on table "public"."token_pack_products" from "service_role";
revoke update on table "public"."token_pack_products" from "service_role";
revoke delete on table "public"."token_transactions" from "anon";
revoke insert on table "public"."token_transactions" from "anon";
revoke references on table "public"."token_transactions" from "anon";
revoke select on table "public"."token_transactions" from "anon";
revoke trigger on table "public"."token_transactions" from "anon";
revoke truncate on table "public"."token_transactions" from "anon";
revoke update on table "public"."token_transactions" from "anon";
revoke delete on table "public"."token_transactions" from "authenticated";
revoke insert on table "public"."token_transactions" from "authenticated";
revoke references on table "public"."token_transactions" from "authenticated";
revoke select on table "public"."token_transactions" from "authenticated";
revoke trigger on table "public"."token_transactions" from "authenticated";
revoke truncate on table "public"."token_transactions" from "authenticated";
revoke update on table "public"."token_transactions" from "authenticated";
revoke delete on table "public"."token_transactions" from "service_role";
revoke insert on table "public"."token_transactions" from "service_role";
revoke references on table "public"."token_transactions" from "service_role";
revoke select on table "public"."token_transactions" from "service_role";
revoke trigger on table "public"."token_transactions" from "service_role";
revoke truncate on table "public"."token_transactions" from "service_role";
revoke update on table "public"."token_transactions" from "service_role";
revoke delete on table "public"."trial_users" from "anon";
revoke insert on table "public"."trial_users" from "anon";
revoke references on table "public"."trial_users" from "anon";
revoke select on table "public"."trial_users" from "anon";
revoke trigger on table "public"."trial_users" from "anon";
revoke truncate on table "public"."trial_users" from "anon";
revoke update on table "public"."trial_users" from "anon";
revoke delete on table "public"."trial_users" from "authenticated";
revoke insert on table "public"."trial_users" from "authenticated";
revoke references on table "public"."trial_users" from "authenticated";
revoke select on table "public"."trial_users" from "authenticated";
revoke trigger on table "public"."trial_users" from "authenticated";
revoke truncate on table "public"."trial_users" from "authenticated";
revoke update on table "public"."trial_users" from "authenticated";
revoke delete on table "public"."trial_users" from "service_role";
revoke insert on table "public"."trial_users" from "service_role";
revoke references on table "public"."trial_users" from "service_role";
revoke select on table "public"."trial_users" from "service_role";
revoke trigger on table "public"."trial_users" from "service_role";
revoke truncate on table "public"."trial_users" from "service_role";
revoke update on table "public"."trial_users" from "service_role";
alter table "public"."subscriptions" drop constraint "subscriptions_status_check";
alter table "public"."subscriptions" drop constraint "subscriptions_user_id_fkey";
alter table "public"."token_balances" drop constraint "token_balances_balance_check";
alter table "public"."token_balances" drop constraint "token_balances_user_id_fkey";
alter table "public"."token_balances" drop constraint "token_balances_user_id_source_key";
alter table "public"."token_pack_products" drop constraint "token_pack_products_stripe_lookup_key_key";
alter table "public"."token_transactions" drop constraint "token_transactions_user_id_fkey";
alter table "public"."trial_users" drop constraint "trial_users_user_id_fkey";
alter table "public"."trial_users" drop constraint "trial_users_user_id_key";
drop function if exists "public"."credit_purchased_tokens"(p_user_id uuid, p_amount integer, p_reference_id text);
drop function if exists "public"."deduct_tokens"(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text);
drop function if exists "public"."ensure_free_tier_fresh"(p_user_id uuid);
drop function if exists "public"."get_subscription_token_limit"(p_user_id uuid);
drop function if exists "public"."grant_subscription_tokens"(p_user_id uuid, p_token_amount integer, p_expires_at timestamp with time zone);
drop function if exists "public"."handle_mesh_status_update"();
drop function if exists "public"."refund_tokens"(p_user_id uuid, p_operation public.token_operation_type, p_reference_id text);
drop function if exists "public"."reset_free_tier_tokens"();
drop function if exists "public"."user_extradata"(user_id_input uuid);
drop type "public"."user_data";
alter table "public"."subscriptions" drop constraint "subscriptions_pkey";
alter table "public"."token_balances" drop constraint "token_balances_pkey";
alter table "public"."token_costs" drop constraint "token_costs_pkey";
alter table "public"."token_pack_products" drop constraint "token_pack_products_pkey";
alter table "public"."token_transactions" drop constraint "token_transactions_pkey";
alter table "public"."trial_users" drop constraint "trial_users_pkey";
drop index if exists "public"."idx_subscriptions_stripe_customer_id";
drop index if exists "public"."idx_subscriptions_stripe_subscription_id";
drop index if exists "public"."idx_subscriptions_user_id";
drop index if exists "public"."subscriptions_pkey";
drop index if exists "public"."token_balances_pkey";
drop index if exists "public"."token_balances_user_id_source_key";
drop index if exists "public"."token_costs_pkey";
drop index if exists "public"."token_pack_products_pkey";
drop index if exists "public"."token_pack_products_stripe_lookup_key_key";
drop index if exists "public"."token_transactions_pkey";
drop index if exists "public"."trial_users_pkey";
drop index if exists "public"."trial_users_user_id_key";
drop table "public"."subscriptions";
drop table "public"."token_balances";
drop table "public"."token_costs";
drop table "public"."token_pack_products";
drop table "public"."token_transactions";
drop table "public"."trial_users";
drop type "public"."stripe-level";
drop type "public"."subscription_level";
drop type "public"."token_operation_type";
drop type "public"."token_source_type";
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
BEGIN
INSERT INTO public.profiles (user_id, full_name)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
)
);
RETURN NEW;
END;
$function$
;
@@ -0,0 +1,256 @@
-- Add the new AI-SDK-shaped columns to messages. `parts` is a discriminated
-- union of AI SDK UI parts (`{type:'text',...}`, `{type:'tool-...'}`, etc.);
-- `metadata` carries per-message bookkeeping like the `model` that generated
-- the response.
--
-- The migration uses a CREATE-TABLE-and-swap pattern instead of an in-place
-- UPDATE. Bench on 745k rows of prod data: the compute (CASE + jsonb_build)
-- takes 1.3s but rewriting every row in place takes 80s+ because of MVCC,
-- per-row WAL, and TOAST writes on the 968 MB of artifact code being copied
-- into `parts.input`. A bulk INSERT into a fresh table writes page-level
-- WAL, has no dead tuples to clean up, and builds the indexes once at the
-- end — total runtime drops well under a minute.
-- Tune for bulk work. Plain SET (not SET LOCAL) so this file works whether
-- run inside a transaction (supabase db push wraps it implicitly) or in
-- autocommit mode (psql on the direct connection); both forms scope to the
-- current session and don't leak.
SET statement_timeout = 0;
SET maintenance_work_mem = '512MB';
SET max_parallel_maintenance_workers = 4;
-- ============================================================================
-- 1. Build a new messages table with the parts/metadata columns pre-populated.
-- No indexes, constraints, or policies yet — those slow down bulk INSERT.
-- ============================================================================
CREATE TABLE public.messages_new AS
SELECT
m.id,
m.created_at,
m.conversation_id,
m.role,
m.content,
m.parent_message_id,
m.rating,
CASE
WHEN m.content IS NULL OR jsonb_typeof(m.content) <> 'object' THEN '[]'::jsonb
WHEN m.role = 'user' THEN
-- text part
(CASE WHEN COALESCE(m.content->>'text','') <> ''
THEN jsonb_build_array(jsonb_build_object('type','text','text', m.content->>'text'))
ELSE '[]'::jsonb END)
-- file parts from images. Storage path is `${user}/${conv}/${imageId}`
-- with no extension; .png suffix is just a display label.
|| (CASE WHEN jsonb_typeof(m.content->'images') = 'array'
THEN COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'type','file',
'mediaType','image/png',
'filename', img || '.png',
'url', '/storage/v1/object/public/images/' || c.user_id::text || '/' || c.id::text || '/' || img
))
FROM jsonb_array_elements_text(m.content->'images') AS img
), '[]'::jsonb)
ELSE '[]'::jsonb END)
-- data-mesh-context
|| (CASE WHEN jsonb_typeof(m.content->'mesh') = 'object'
AND (m.content->'mesh'->>'id') IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'type','data-mesh-context',
'data', jsonb_strip_nulls(jsonb_build_object(
'meshId', m.content->'mesh'->>'id',
'fileType', COALESCE(m.content->'mesh'->>'fileType','glb'),
'filename', m.content->>'meshFilename',
'boundingBox', m.content->'meshBoundingBox'
))
))
ELSE '[]'::jsonb END)
-- data-mesh-preferences
|| (CASE WHEN (m.content ? 'meshTopology') OR (m.content ? 'polygonCount')
THEN jsonb_build_array(jsonb_build_object(
'type','data-mesh-preferences',
'data', jsonb_build_object(
'topology', COALESCE(m.content->>'meshTopology','polys'),
'polygonCount', COALESCE(NULLIF(m.content->>'polygonCount','')::int, 100000)
)
))
ELSE '[]'::jsonb END)
WHEN m.role = 'assistant' THEN
-- text part (assistant text lands with state:'done')
(CASE WHEN COALESCE(m.content->>'text','') <> ''
THEN jsonb_build_array(jsonb_build_object(
'type','text','text', m.content->>'text','state','done'))
ELSE '[]'::jsonb END)
-- tool-build_parametric_model. The legacy toolCalls array was the
-- source of the original tool call id, but ~98% of assistant rows have
-- toolCalls=[], so we synthesize a deterministic id keyed on the
-- message uuid instead. Re-runs are byte-identical for the same row.
-- The artifact object is referenced directly rather than rebuilt
-- field-by-field — same data, no re-serialization cost.
|| (CASE WHEN jsonb_typeof(m.content->'artifact') = 'object'
THEN jsonb_build_array(jsonb_build_object(
'type','tool-build_parametric_model',
'toolCallId', 'tool_legacy_bp_' || m.id::text,
'state','output-available',
'input', m.content->'artifact',
'output', jsonb_build_object('status','success','message','')
))
ELSE '[]'::jsonb END)
-- tool-create_mesh
|| (CASE WHEN jsonb_typeof(m.content->'mesh') = 'object'
AND (m.content->'mesh'->>'id') IS NOT NULL
THEN jsonb_build_array(jsonb_build_object(
'type','tool-create_mesh',
'toolCallId', m.content->'mesh'->>'id',
'state','output-available',
'input', '{}'::jsonb,
'output', jsonb_build_object(
'id', m.content->'mesh'->>'id',
'fileType', COALESCE(m.content->'mesh'->>'fileType','glb')
)
))
ELSE '[]'::jsonb END)
-- assistant-side file parts (legacy create_image tool outputs)
|| (CASE WHEN jsonb_typeof(m.content->'images') = 'array'
THEN COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'type','file',
'mediaType','image/png',
'filename', img || '.png',
'url', '/storage/v1/object/public/images/' || c.user_id::text || '/' || c.id::text || '/' || img
))
FROM jsonb_array_elements_text(m.content->'images') AS img
), '[]'::jsonb)
ELSE '[]'::jsonb END)
ELSE '[]'::jsonb
END AS parts,
jsonb_strip_nulls(jsonb_build_object('model', m.content->>'model')) AS metadata
FROM public.messages m
LEFT JOIN public.conversations c ON c.id = m.conversation_id;
-- ============================================================================
-- 2. Restore the column constraints CTAS strips out. `content` becomes
-- nullable (new code writes parts only); `parts` and `metadata` get the
-- same defaults as the original ADD COLUMN form.
-- ============================================================================
ALTER TABLE public.messages_new
ALTER COLUMN id SET DEFAULT gen_random_uuid(),
ALTER COLUMN id SET NOT NULL,
ALTER COLUMN created_at SET DEFAULT now(),
ALTER COLUMN created_at SET NOT NULL,
ALTER COLUMN conversation_id SET NOT NULL,
ALTER COLUMN role SET NOT NULL,
ALTER COLUMN rating SET DEFAULT 0::smallint,
ALTER COLUMN rating SET NOT NULL,
ALTER COLUMN parts SET DEFAULT '[]'::jsonb,
ALTER COLUMN parts SET NOT NULL,
ALTER COLUMN metadata SET DEFAULT '{}'::jsonb,
ALTER COLUMN metadata SET NOT NULL;
-- ============================================================================
-- 3. Indexes. Built once at the end of the bulk load — far faster than
-- maintaining them per-row during INSERT.
-- ============================================================================
ALTER TABLE public.messages_new
ADD CONSTRAINT messages_new_pkey PRIMARY KEY (id);
CREATE INDEX messages_new_conversation_id_idx
ON public.messages_new USING btree (conversation_id);
-- ============================================================================
-- 4. Constraints. The role check matches the original schema. The
-- payload-present check is new and added NOT VALID to skip the full
-- table scan — every row trivially satisfies it (content was NOT NULL
-- in the legacy schema and every row carries either parts or content).
-- ============================================================================
ALTER TABLE public.messages_new
ADD CONSTRAINT messages_role_check
CHECK (role = ANY (ARRAY['user'::text, 'assistant'::text])) NOT VALID;
ALTER TABLE public.messages_new
ADD CONSTRAINT messages_payload_present
CHECK (jsonb_array_length(parts) > 0 OR content IS NOT NULL) NOT VALID;
ALTER TABLE public.messages_new
ADD CONSTRAINT messages_new_conversation_id_fkey
FOREIGN KEY (conversation_id) REFERENCES public.conversations(id)
ON DELETE CASCADE NOT VALID;
-- ============================================================================
-- 5. RLS — same policies as the legacy table.
-- ============================================================================
ALTER TABLE public.messages_new ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public conversations messages"
ON public.messages_new
FOR SELECT
TO anon, authenticated
USING (EXISTS (
SELECT 1 FROM public.conversations
WHERE conversations.id = messages_new.conversation_id
AND conversations.privacy = 'public'::privacy_type
));
CREATE POLICY "Users can manage messages in their conversations"
ON public.messages_new
USING ((SELECT auth.uid() AS uid) IN (
SELECT conversations.user_id FROM public.conversations
WHERE conversations.id = messages_new.conversation_id
));
-- ============================================================================
-- 6. Grants — mirror the legacy table's permissions for anon/authenticated/
-- service_role/postgres.
-- ============================================================================
GRANT ALL ON TABLE public.messages_new TO anon;
GRANT ALL ON TABLE public.messages_new TO authenticated;
GRANT ALL ON TABLE public.messages_new TO service_role;
GRANT ALL ON TABLE public.messages_new TO postgres;
-- ============================================================================
-- 7. Trigger — keep conversations.current_message_leaf_id pointed at the
-- latest inserted message (function is unchanged, defined in the initial
-- schema migration).
-- ============================================================================
CREATE TRIGGER update_leaf_trigger
AFTER INSERT ON public.messages_new
FOR EACH ROW EXECUTE FUNCTION public.update_conversation_leaf();
-- ============================================================================
-- 8. Atomic swap. Drop the old table, rename the new one into its place,
-- rename the indexes/constraints so future migrations and ORMs see the
-- canonical names. This whole step holds an ACCESS EXCLUSIVE lock on
-- `messages` for milliseconds.
-- ============================================================================
DROP TABLE public.messages;
ALTER TABLE public.messages_new RENAME TO messages;
ALTER INDEX public.messages_new_pkey RENAME TO messages_pkey;
ALTER INDEX public.messages_new_conversation_id_idx RENAME TO messages_conversation_id_idx;
ALTER TABLE public.messages RENAME CONSTRAINT messages_new_conversation_id_fkey TO messages_conversation_id_fkey;
-- ============================================================================
-- 9. The legacy `conversations.legacy` column is no longer needed.
-- ============================================================================
ALTER TABLE public.conversations DROP COLUMN IF EXISTS legacy;
-- ============================================================================
-- 10. Atomic write for `conversations.settings.suggestions`. The chat server
-- previously read settings, merged the new suggestions array client-side,
-- and wrote the whole object back — which under concurrent assistant
-- turns could silently drop the other update. `jsonb_set` runs in one
-- statement so only the `suggestions` key is touched.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.set_conversation_suggestions(
p_conversation_id uuid,
p_suggestions jsonb
) RETURNS void LANGUAGE sql VOLATILE SECURITY INVOKER AS $$
UPDATE public.conversations
SET settings = jsonb_set(
COALESCE(settings, '{}'::jsonb),
'{suggestions}',
p_suggestions,
true
)
WHERE id = p_conversation_id;
$$;
@@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS "public"."conversations" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"(),
"updated_at" timestamp with time zone DEFAULT "now"(),
"user_id" "uuid" NOT NULL,
"title" "text" NOT NULL,
"type" "public"."conversation-type" DEFAULT 'parametric'::"public"."conversation-type" NOT NULL,
"privacy" "public"."privacy_type" DEFAULT 'private'::"public"."privacy_type" NOT NULL,
"current_message_leaf_id" "uuid",
"settings" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS conversations_pkey ON "public"."conversations" USING btree (id);
ALTER TABLE "public"."conversations" ADD CONSTRAINT "conversations_pkey" PRIMARY KEY USING INDEX "conversations_pkey";
ALTER TABLE "public"."conversations" ADD CONSTRAINT "conversations_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."conversations" VALIDATE CONSTRAINT "conversations_user_id_fkey";
CREATE INDEX IF NOT EXISTS conversations_created_at_idx ON "public"."conversations" USING btree (created_at);
CREATE INDEX IF NOT EXISTS conversations_updated_at_idx ON "public"."conversations" USING btree (updated_at);
CREATE INDEX IF NOT EXISTS conversations_user_id_idx ON "public"."conversations" USING btree (user_id);
CREATE POLICY "Anyone can view a public conversation" ON "public"."conversations" FOR SELECT TO "authenticated", "anon" USING (("privacy" = 'public'::"public"."privacy_type"));
CREATE POLICY "Users can manage their own conversations" ON "public"."conversations" USING ( (SELECT "auth"."uid"()) = "user_id" );
ALTER TABLE "public"."conversations" ENABLE ROW LEVEL SECURITY;
+34
View File
@@ -0,0 +1,34 @@
CREATE TABLE IF NOT EXISTS "public"."images" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
"user_id" "uuid" NOT NULL,
"conversation_id" "uuid" NOT NULL,
"image_generation_call_id" "text",
"prompt" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS images_pkey ON "public"."images" USING btree (id);
ALTER TABLE "public"."images" ADD CONSTRAINT "images_pkey" PRIMARY KEY USING INDEX "images_pkey";
ALTER TABLE "public"."images" ADD CONSTRAINT "images_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."images" VALIDATE CONSTRAINT "images_conversation_id_fkey";
ALTER TABLE "public"."images" ADD CONSTRAINT "images_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."images" VALIDATE CONSTRAINT "images_user_id_fkey";
CREATE INDEX IF NOT EXISTS idx_images_image_generation_call_id ON "public"."images" USING "btree" ("image_generation_call_id");
CREATE POLICY "Public conversations images" ON "public"."images" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
FROM "public"."conversations"
WHERE (("conversations"."id" = "images"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
CREATE POLICY "User can manage their data" ON "public"."images" TO "authenticated" USING ((( SELECT "auth"."uid"()) = "user_id")) WITH CHECK ((( SELECT "auth"."uid"()) = "user_id"));
ALTER TABLE "public"."images" ENABLE ROW LEVEL SECURITY;
+32
View File
@@ -0,0 +1,32 @@
CREATE TABLE IF NOT EXISTS "public"."meshes" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
"user_id" "uuid" NOT NULL,
"images" "uuid"[],
"conversation_id" "uuid" NOT NULL,
"prompt" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
"file_type" "public"."mesh_file_type" DEFAULT 'glb'::"public"."mesh_file_type" NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS meshes_pkey ON "public"."meshes" USING btree (id);
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_pkey" PRIMARY KEY USING INDEX "meshes_pkey";
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."meshes" VALIDATE CONSTRAINT "meshes_conversation_id_fkey";
ALTER TABLE "public"."meshes" ADD CONSTRAINT "meshes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."meshes" VALIDATE CONSTRAINT "meshes_user_id_fkey";
CREATE POLICY "Everyone can view meshes associated with public conversations" ON "public"."meshes" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
FROM "public"."conversations"
WHERE (("conversations"."id" = "meshes"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
CREATE POLICY "Users can manage their meshes" ON "public"."meshes" USING ( (SELECT "auth"."uid"()) = "user_id" );
ALTER TABLE "public"."meshes" ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS "public"."messages" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"conversation_id" "uuid" NOT NULL,
"role" "text" NOT NULL,
"parts" "jsonb" DEFAULT '[]'::"jsonb" NOT NULL,
"metadata" "jsonb" DEFAULT '{}'::"jsonb" NOT NULL,
"content" "jsonb",
"rating" smallint DEFAULT '0'::smallint NOT NULL,
"parent_message_id" "uuid",
CONSTRAINT "messages_role_check" CHECK (("role" = ANY (ARRAY['user'::"text", 'assistant'::"text"]))),
CONSTRAINT "messages_payload_present" CHECK ((jsonb_array_length("parts") > 0) OR ("content" IS NOT NULL))
);
CREATE UNIQUE INDEX IF NOT EXISTS messages_pkey ON "public"."messages" USING btree (id);
ALTER TABLE "public"."messages" ADD CONSTRAINT "messages_pkey" PRIMARY KEY USING INDEX "messages_pkey";
ALTER TABLE "public"."messages" ADD CONSTRAINT "messages_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE not valid;
ALTER TABLE "public"."messages" VALIDATE CONSTRAINT "messages_conversation_id_fkey";
CREATE INDEX IF NOT EXISTS messages_conversation_id_idx ON "public"."messages" USING btree (conversation_id);
CREATE POLICY "Public conversations messages" ON "public"."messages" FOR SELECT TO "authenticated", "anon" USING ((EXISTS ( SELECT 1
FROM "public"."conversations"
WHERE (("conversations"."id" = "messages"."conversation_id") AND ("conversations"."privacy" = 'public'::"public"."privacy_type")))));
CREATE POLICY "Users can manage messages in their conversations" ON "public"."messages" USING (((SELECT "auth"."uid"()) IN ( SELECT "conversations"."user_id"
FROM "public"."conversations"
WHERE ("conversations"."id" = "messages"."conversation_id"))));
ALTER TABLE "public"."messages" ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS "public"."previews" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"status" "public"."generation-status" DEFAULT 'pending'::"public"."generation-status" NOT NULL,
"user_id" "uuid" NOT NULL,
"conversation_id" "uuid" NOT NULL,
"mesh_id" "uuid" NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS previews_pkey ON "public"."previews" USING btree (id);
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_pkey" PRIMARY KEY USING INDEX "previews_pkey";
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_user_id_fkey";
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_conversation_id_fkey" FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_conversation_id_fkey";
ALTER TABLE "public"."previews" ADD CONSTRAINT "previews_mesh_id_fkey" FOREIGN KEY (mesh_id) REFERENCES meshes(id) ON DELETE CASCADE not valid;
ALTER TABLE "public"."previews" VALIDATE CONSTRAINT "previews_mesh_id_fkey";
ALTER TABLE "public"."previews" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage their own previews" ON "public"."previews" USING ( (SELECT "auth"."uid"()) = "user_id" );
+21
View File
@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS "public"."profiles" (
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
"created_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"updated_at" timestamp with time zone DEFAULT "now"() NOT NULL,
"user_id" "uuid" NOT NULL,
"full_name" "text" NOT NULL,
"notifications_enabled" boolean DEFAULT false NOT NULL,
"avatar_path" "text" DEFAULT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS profiles_pkey ON "public"."profiles" USING btree (id);
ALTER TABLE "public"."profiles" ADD CONSTRAINT "profiles_pkey" PRIMARY KEY USING INDEX "profiles_pkey";
ALTER TABLE "public"."profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."profiles" VALIDATE CONSTRAINT "profiles_user_id_fkey";
CREATE POLICY "Users can manage their own profile" ON "public"."profiles" USING ( (SELECT "auth"."uid"()) = "user_id" );
ALTER TABLE "public"."profiles" ENABLE ROW LEVEL SECURITY;
+18
View File
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS "public"."prompts" (
"id" bigint generated by default as identity not null,
"created_at" timestamp with time zone not null default now(),
"user_id" "uuid" NOT NULL,
"type" "public"."prompt_type" DEFAULT 'chat'::"public"."prompt_type" NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS prompts_pkey ON "public"."prompts" USING btree (id);
ALTER TABLE "public"."prompts" ADD CONSTRAINT "prompts_pkey" PRIMARY KEY USING INDEX "prompts_pkey";
ALTER TABLE "public"."prompts" ADD CONSTRAINT "prompts_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
ALTER TABLE "public"."prompts" VALIDATE CONSTRAINT "prompts_user_id_fkey";
CREATE POLICY "Users can view their own prompts" ON "public"."prompts" FOR SELECT USING ((SELECT "auth"."uid"()) = "user_id");
ALTER TABLE "public"."prompts" ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,48 @@
-- Images and Meshes
CREATE POLICY "Give users access to own folder images_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder images_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder images_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder images_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'images' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder meshes_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder meshes_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder meshes_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder meshes_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'meshes' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
-- Shared Conversations
CREATE POLICY "Public conversations allow anyone to view images_select" ON storage.objects FOR SELECT TO anon, authenticated USING (((bucket_id = 'images'::text) AND (EXISTS ( SELECT 1 FROM conversations WHERE ((conversations.privacy = 'public') AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
CREATE POLICY "Public conversations allow anyone to view meshes_select" ON storage.objects FOR SELECT TO anon, authenticated USING (((bucket_id = 'meshes'::text) AND (EXISTS ( SELECT 1 FROM conversations WHERE ((conversations.privacy = 'public') AND ((conversations.id)::text = (storage.foldername(objects.name))[2]))))));
-- Previews
CREATE POLICY "Give users access to own folder previews_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder previews_insert" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder previews_update" ON storage.objects FOR UPDATE TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
CREATE POLICY "Give users access to own folder previews_delete" ON storage.objects FOR DELETE TO public USING (bucket_id = 'previews' AND (select auth.uid()::text) = (storage.foldername(name))[1]);
-- Only allow service role to upload temp multiview images (from our functions)
CREATE POLICY "Allow service role to upload temp multiview images"
ON storage.objects FOR INSERT
TO service_role
WITH CHECK (bucket_id = 'temp-multiview');
-- Allow public read access (for Tripo to download)
CREATE POLICY "Allow public read access to temp multiview images"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'temp-multiview');
-- Allow service role to delete expired files (for manual cleanup if needed)
CREATE POLICY "Allow service role to delete temp multiview images"
ON storage.objects FOR DELETE
TO service_role
USING (bucket_id = 'temp-multiview');
@@ -0,0 +1,54 @@
CREATE OR REPLACE FUNCTION "public"."update_conversation_leaf"() RETURNS "trigger"
LANGUAGE "plpgsql"
AS $$
begin
update conversations set
current_message_leaf_id = new.id,
updated_at = now()
where id = new.conversation_id;
return new;
end;
$$;
CREATE OR REPLACE TRIGGER "update_leaf_trigger" AFTER INSERT ON "public"."messages" FOR EACH ROW EXECUTE FUNCTION "public"."update_conversation_leaf"();
-- Previews updated_at trigger
-- Function to update the updated_at column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Trigger to automatically update updated_at for previews
CREATE OR REPLACE TRIGGER update_previews_updated_at
BEFORE UPDATE ON "public"."previews"
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Profile creation trigger for new users
-- Create function to handle new user sign ups
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER SET search_path = ''
AS $$
BEGIN
INSERT INTO public.profiles (user_id, full_name)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data->>'full_name',
split_part(NEW.email, '@', 1)
)
);
RETURN NEW;
END;
$$;
-- Create trigger to automatically create profile on user creation
CREATE OR REPLACE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
+34
View File
@@ -0,0 +1,34 @@
CREATE TYPE "public"."conversation-type" AS ENUM (
'parametric',
'creative'
);
CREATE TYPE "public"."generation-status" AS ENUM (
'pending',
'success',
'failure'
);
CREATE TYPE "public"."mesh_model_type" AS ENUM (
'quality',
'fast'
);
CREATE TYPE "public"."mesh_file_type" AS ENUM (
'glb',
'stl',
'obj',
'fbx'
);
CREATE TYPE "public"."privacy_type" AS ENUM (
'public',
'private'
);
CREATE TYPE "public"."prompt_type" AS ENUM (
'mesh',
'image',
'chat'
);
+10
View File
@@ -0,0 +1,10 @@
-- THIS TEST USER IS USED FOR TESTING PURPOSES ONLY ON LOCAL DEVELOPMENT DONT RUN THIS IN PRODUCTION OR MOM WILL BE SAD
INSERT INTO auth.users ( instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, recovery_sent_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, created_at, updated_at, confirmation_token, email_change, email_change_token_new, recovery_token)
VALUES
('00000000-0000-0000-0000-000000000000', uuid_generate_v4(), 'authenticated', 'authenticated', 'test@adamcad.com', crypt('password', gen_salt('bf')), current_timestamp, current_timestamp, current_timestamp, '{"provider":"email","providers":["email"]}', '{}', current_timestamp, current_timestamp, '', '', '', '');
-- test user email identity
INSERT INTO auth.identities (id, user_id, identity_data, provider, provider_id, last_sign_in_at, created_at, updated_at)
VALUES
(uuid_generate_v4(), (SELECT id FROM auth.users WHERE email = 'test@adamcad.com'), format('{"sub":"%s","email":"%s"}', (SELECT id FROM auth.users WHERE email = 'test@adamcad.com')::text, 'test@adamcad.com')::jsonb, 'email', uuid_generate_v4(), current_timestamp, current_timestamp, current_timestamp);