Skip to content

Commit 0ede6ea

Browse files
LEANDERANTONYclaude
andcommitted
Supabase: drop legacy saved_workspaces sweeper + harden RPC grants
Two clean-up doc updates triggered by an audit of the prod Supabase state after the tier-enforcement rollout: docs/sql/supabase-bootstrap.sql * Remove cleanup_expired_saved_workspaces RPC + pg_cron block. Step 8 of tier-enforcement shipped a tier-aware Python sweeper in backend/maintenance.py that does this same DELETE with Free 7d / Pro 30d / Business unbounded retention, plus routes through resolve_user_tier so Stripe wires retention with a single switch. Both running in parallel raced (pg_cron deleting before the Python sweep iterated) and broke tier semantics for Business users. Applied to prod by Supabase migration `drop_legacy_saved_workspaces_cleanup` (20260514183110). docs/sql/supabase-resume-builder.sql * Add `revoke all from public/anon/authenticated` on cleanup_expired_resume_builder_sessions. Without these, Supabase grants EXECUTE to all client-facing roles by default on public- schema functions, which means any caller with the public anon key could call the RPC via /rest/v1/rpc/<name> and trigger arbitrary expired-session cleanup. Mirrored the same fix the tier-enforcement work backported to the quota counters migration. Applied to prod by `revoke_anon_from_existing_definer_rpcs` (20260514183200) + `revoke_public_from_existing_definer_rpcs`. The search_cached_jobs_ranked RPC is locked down the same way on prod (postgres + service_role only) but lives entirely in the Supabase migration history rather than in this repo's docs/sql/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e6f13c3 commit 0ede6ea

2 files changed

Lines changed: 37 additions & 41 deletions

File tree

docs/sql/supabase-bootstrap.sql

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -168,25 +168,26 @@ alter table public.saved_jobs enable row level security;
168168

169169
create extension if not exists pg_cron with schema extensions;
170170

171-
create or replace function public.cleanup_expired_saved_workspaces()
172-
returns integer
173-
language plpgsql
174-
security definer
175-
set search_path = public
176-
as $$
177-
declare
178-
deleted_count integer := 0;
179-
begin
180-
delete from public.saved_workspaces
181-
where expires_at <= timezone('utc', now());
182-
183-
get diagnostics deleted_count = row_count;
184-
return deleted_count;
185-
end;
186-
$$;
187-
188-
revoke all on function public.cleanup_expired_saved_workspaces() from public;
189-
grant execute on function public.cleanup_expired_saved_workspaces() to service_role;
171+
-- Note: the saved-workspaces retention path was originally a SQL-only
172+
-- sweeper (cleanup_expired_saved_workspaces RPC) running on a 5-minute
173+
-- pg_cron schedule. Step 8 of the tier-enforcement series replaced
174+
-- that with a Python sweeper in backend/maintenance.py that:
175+
-- 1. Does what this RPC did (DELETE expired saved_workspaces rows)
176+
-- 2. Is tier-aware (Free 7d / Pro 30d / Business unbounded) instead
177+
-- of the single hardcoded expires_at-based deletion
178+
-- 3. Routes through resolve_user_tier so payment integration flips
179+
-- retention semantics with a single switch
180+
--
181+
-- The Python sweeper is scheduled via VPS crontab (daily):
182+
-- 17 3 * * * cd /app && python -m backend.maintenance >> /var/log/maintenance.log 2>&1
183+
--
184+
-- Both running in parallel would race: pg_cron could delete a row
185+
-- before the Python sweeper iterated it, breaking tier semantics for
186+
-- Business users whose expires_at was set under the old default.
187+
--
188+
-- Applied to prod by Supabase migration `drop_legacy_saved_workspaces_cleanup`
189+
-- (20260514183110). Git history of this file preserves the original
190+
-- RPC + cron block before the cleanup.
190191

191192
drop policy if exists "users can read own saved workspace" on public.saved_workspaces;
192193
create policy "users can read own saved workspace"
@@ -249,25 +250,9 @@ for delete
249250
to authenticated
250251
using (auth.uid() = user_id);
251252

252-
select public.cleanup_expired_saved_workspaces();
253-
254-
do $$
255-
begin
256-
if exists (
257-
select 1
258-
from cron.job
259-
where jobname = 'cleanup-expired-saved-workspaces'
260-
) then
261-
perform cron.unschedule('cleanup-expired-saved-workspaces');
262-
end if;
263-
exception
264-
when undefined_table then
265-
null;
266-
end;
267-
$$;
268-
269-
select cron.schedule(
270-
'cleanup-expired-saved-workspaces',
271-
'*/5 * * * *',
272-
$$select public.cleanup_expired_saved_workspaces();$$
273-
);
253+
-- The legacy `cleanup-expired-saved-workspaces` pg_cron job + manual
254+
-- invocation (formerly here) were removed alongside the RPC. See the
255+
-- comment above the (removed) cleanup_expired_saved_workspaces RPC
256+
-- block earlier in this file for the rationale and replacement path.
257+
-- The VPS-side Python sweeper at backend/maintenance.py is the
258+
-- single source of truth for saved_workspaces retention.

docs/sql/supabase-resume-builder.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ begin
7373
end;
7474
$$;
7575

76+
-- Lock down execution. Without this, Supabase grants EXECUTE on
77+
-- public-schema functions to PUBLIC, anon, and authenticated by
78+
-- default — meaning any caller with the public anon key could trigger
79+
-- arbitrary expired-session cleanup via the REST RPC surface. The
80+
-- pg_cron job runs as the cron owner (postgres) so the unschedule/
81+
-- schedule below still work; the backend never calls this function
82+
-- directly. (Mirrored on prod by migration `revoke_public_from_existing_definer_rpcs`.)
83+
revoke all on function public.cleanup_expired_resume_builder_sessions() from public;
84+
revoke all on function public.cleanup_expired_resume_builder_sessions() from anon;
85+
revoke all on function public.cleanup_expired_resume_builder_sessions() from authenticated;
86+
7687
-- Reset the cron schedule idempotently so re-running this file
7788
-- doesn't queue duplicate jobs.
7889
do $$

0 commit comments

Comments
 (0)