Add local dev environment: Supabase config, base schema bootstrap, AGENTS.md#412
Merged
Merged
Conversation
…ENTS.md - supabase/config.toml: local stack config (pgmq_public exposed to PostgREST, avatars bucket, localhost auth redirects, edge runtime disabled) - 00000000000000_local_base_schema.sql: guarded reconstruction of the hosted base schema (tables, triggers, RPCs, pgmq_public wrappers, storage policies) so fresh local supabase start/db reset works - Rename two migrations that shared a version prefix (20260515, 20260526) with another file, which breaks the Supabase CLI's unique-version tracking; contents unchanged and idempotent - AGENTS.md: Cursor Cloud dev environment notes Co-authored-by: Lee Robinson <lee@leerob.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Install RPC publicly callable
- Revoked the default PUBLIC/anon/authenticated EXECUTE on increment_install_count and re-granted only to service_role, matching how it is invoked from the admin client.
- ✅ Fixed: Avatars bucket lacks path ownership
- Scoped the avatars insert/update policies to owner_id = auth.uid() (the bucket is shared across user/company/plugin/mcp uploads with mixed path prefixes, so ownership is the correct guard rather than the path segment).
- ✅ Fixed: Public tables missing RLS
- Enabled RLS on all seven public tables and added owner-scoped policies (public reads where the app relies on them), verified to preserve every app flow and admin-client bypass while blocking cross-user reads/writes.
Or push these changes by commenting:
@cursor push 78ce43b59f
Preview (78ce43b59f)
diff --git a/supabase/migrations/00000000000000_local_base_schema.sql b/supabase/migrations/00000000000000_local_base_schema.sql
--- a/supabase/migrations/00000000000000_local_base_schema.sql
+++ b/supabase/migrations/00000000000000_local_base_schema.sql
@@ -528,11 +528,126 @@
end$$;
-- ---------------------------------------------------------------------------
--- Storage: policies for the public `avatars` bucket (bucket itself is
--- declared in supabase/config.toml for local dev).
+-- Function privileges: increment_install_count is invoked only from the
+-- service-role admin client (src/actions/track-install.ts). Postgres grants
+-- EXECUTE to PUBLIC by default, so without this any anon/authenticated caller
+-- could inflate plugins.install_count via PostgREST. Revoke it and re-grant to
+-- service_role only (mirrors the hardening on later RPCs such as
+-- update_plugin_with_components and toggle_plugin_star).
-- ---------------------------------------------------------------------------
+revoke execute on function public.increment_install_count(uuid) from public, anon, authenticated;
+grant execute on function public.increment_install_count(uuid) to service_role;
+
+-- ---------------------------------------------------------------------------
+-- Row Level Security. The hosted base schema enforces RLS on these tables and
+-- the app's browser/cookie-scoped clients (anon / authenticated) rely on it,
+-- while the service-role admin client bypasses RLS. Enable RLS and add policies
+-- that mirror that posture so a fresh local database matches production instead
+-- of leaving every row world-readable/writable.
+-- ---------------------------------------------------------------------------
do $$
+declare t text;
begin
+ foreach t in array array[
+ 'users', 'companies', 'plugins', 'plugin_components', 'plugin_stars',
+ 'followers', 'mcps'
+ ] loop
+ if not exists (
+ select 1 from pg_class c join pg_namespace n on n.oid = c.relnamespace
+ where n.nspname = 'public' and c.relname = t and c.relrowsecurity
+ ) then
+ execute format('alter table public.%I enable row level security', t);
+ end if;
+ end loop;
+end$$;
+
+do $$
+begin
+ -- users: a session may read and edit only its own profile row. Public
+ -- profile and directory reads run through the service-role admin client.
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'users' and policyname = 'users_select_own') then
+ create policy users_select_own on public.users
+ for select to authenticated using ((select auth.uid()) = id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'users' and policyname = 'users_update_own') then
+ create policy users_update_own on public.users
+ for update to authenticated using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
+ end if;
+
+ -- companies: public rows are readable by anyone (directory search) and a user
+ -- always sees and owns the companies they created.
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'companies' and policyname = 'companies_select_public_or_own') then
+ create policy companies_select_public_or_own on public.companies
+ for select using (public = true or (select auth.uid()) = owner_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'companies' and policyname = 'companies_insert_own') then
+ create policy companies_insert_own on public.companies
+ for insert to authenticated with check ((select auth.uid()) = owner_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'companies' and policyname = 'companies_update_own') then
+ create policy companies_update_own on public.companies
+ for update to authenticated using ((select auth.uid()) = owner_id) with check ((select auth.uid()) = owner_id);
+ end if;
+
+ -- plugins: active listings are world-readable; owners read/modify their own
+ -- rows (seed rows have a null owner_id and are reachable only via active).
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'plugins' and policyname = 'plugins_select_active_or_own') then
+ create policy plugins_select_active_or_own on public.plugins
+ for select using (active = true or (select auth.uid()) = owner_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'plugins' and policyname = 'plugins_update_own') then
+ create policy plugins_update_own on public.plugins
+ for update to authenticated using ((select auth.uid()) = owner_id) with check ((select auth.uid()) = owner_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'plugins' and policyname = 'plugins_delete_own') then
+ create policy plugins_delete_own on public.plugins
+ for delete to authenticated using ((select auth.uid()) = owner_id);
+ end if;
+
+ -- followers: a user manages only the rows where they are the follower.
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'followers' and policyname = 'followers_select_own') then
+ create policy followers_select_own on public.followers
+ for select to authenticated using ((select auth.uid()) = follower_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'followers' and policyname = 'followers_insert_own') then
+ create policy followers_insert_own on public.followers
+ for insert to authenticated with check ((select auth.uid()) = follower_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'followers' and policyname = 'followers_delete_own') then
+ create policy followers_delete_own on public.followers
+ for delete to authenticated using ((select auth.uid()) = follower_id);
+ end if;
+
+ -- plugin_stars: a user reads only their own stars; writes go through the
+ -- SECURITY DEFINER toggle_plugin_star RPC (20260523_plugin_star_atomic.sql).
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'plugin_stars' and policyname = 'plugin_stars_select_own') then
+ create policy plugin_stars_select_own on public.plugin_stars
+ for select to authenticated using ((select auth.uid()) = user_id);
+ end if;
+
+ -- mcps: active listings are world-readable; owners modify their own rows.
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'mcps' and policyname = 'mcps_select_active_or_own') then
+ create policy mcps_select_active_or_own on public.mcps
+ for select using (active = true or (select auth.uid()) = owner_id);
+ end if;
+ if not exists (select 1 from pg_policies where schemaname = 'public' and tablename = 'mcps' and policyname = 'mcps_update_own') then
+ create policy mcps_update_own on public.mcps
+ for update to authenticated using ((select auth.uid()) = owner_id) with check ((select auth.uid()) = owner_id);
+ end if;
+
+ -- plugin_components is intentionally left without an anon/authenticated
+ -- policy: it is written by the admin client and read via plugin joins on the
+ -- server, so RLS denies all direct Data API access (service_role bypasses it).
+end$$;
+
+-- ---------------------------------------------------------------------------
+-- Storage: policies for the public `avatars` bucket (bucket itself is declared
+-- in supabase/config.toml). The bucket is shared across user, company, plugin
+-- and mcp uploads with mixed path prefixes, so write access is scoped to the
+-- object owner (owner_id) rather than the first path segment.
+-- ---------------------------------------------------------------------------
+do $$
+begin
if not exists (
select 1 from pg_policies
where schemaname = 'storage' and tablename = 'objects'
@@ -548,7 +663,8 @@
and policyname = 'avatars_authenticated_insert'
) then
create policy avatars_authenticated_insert on storage.objects
- for insert to authenticated with check (bucket_id = 'avatars');
+ for insert to authenticated
+ with check (bucket_id = 'avatars' and owner_id = (select auth.uid())::text);
end if;
if not exists (
@@ -557,6 +673,8 @@
and policyname = 'avatars_authenticated_update'
) then
create policy avatars_authenticated_update on storage.objects
- for update to authenticated using (bucket_id = 'avatars');
+ for update to authenticated
+ using (bucket_id = 'avatars' and owner_id = (select auth.uid())::text)
+ with check (bucket_id = 'avatars' and owner_id = (select auth.uid())::text);
end if;
end$$;You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 4069c01. Configure here.
Collaborator
Author
…rage) The local bootstrap schema diverged from the hosted project's security posture, leaving a fresh local database far more permissive than production: - Enable Row Level Security on the public application tables (users, companies, plugins, plugin_components, plugin_stars, followers, mcps) and add owner-scoped policies. The app's browser/cookie-scoped clients (anon/authenticated) rely on RLS; the service-role admin client bypasses it. Without RLS, authenticated clients could read and mutate rows they do not own. - Revoke the default PUBLIC EXECUTE grant on increment_install_count and re-grant only to service_role. It is called solely from the admin client (track-install.ts), so leaving it PUBLIC let any key holder inflate install_count via PostgREST (mirrors update_plugin_with_components / toggle_plugin_star hardening). - Scope the avatars bucket insert/update policies to the object owner_id instead of bucket-only. The bucket is shared across user/company/plugin/ mcp uploads with mixed path prefixes, so ownership (not the path prefix) is the correct guard against overwriting other users' files. All guarded for idempotency (no-op when objects already exist). Applied via @cursor push command
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Makes the repo runnable locally with the Supabase CLI (
supabase start→bun dev), and documents cloud-agent dev environment caveats.supabase/config.toml(+ generatedsupabase/.gitignore): local stack config — exposespgmq_publicto PostgREST (required bysrc/lib/plugins/queue.ts), declares the publicavatarsbucket, allowshttp://localhost:3000auth redirects, and disables the unused edge runtime.supabase/migrations/00000000000000_local_base_schema.sql: guarded reconstruction of the hosted project's pre-existing base schema (tables, signup/slug/follow-count triggers,increment_install_count& legacy star RPCs,pgmq_publicwrappers, storage policies). Every statement is existence-checked so it is a no-op where objects already exist; nothing is ever replaced.20260515_plugin_similar_search.sql→20260516_...and20260526_companies_unique_name.sql→20260527_...(contents unchanged, both idempotent): two pairs of migrations shared a version prefix, which violates the Supabase CLI's unique-version tracking and breakssupabase db reset/db push.AGENTS.md: Cursor Cloud specific instructions (service startup, seeding gotchas, auth-locally notes).Verification
Full local flow verified end to end: fresh
supabase db resetapplies all migrations cleanly; seeded 9 real plugins via the repo'sseed:extract/seed:insertscripts; submittedcursor/plugin-templatethrough the UI, approved it from/admin/plugins, starred it, and confirmed the pgmq scan queue enqueue/drain works (scan itself errors withoutCURSOR_API_KEY, as expected locally).bunx biome ci .bun run typecheck(apps/cursor)supabase db reset(all migrations apply on fresh DB)