Skip to content

Commit f521f8b

Browse files
authored
Seed local Supabase media (#35)
* seed local supabase media * harden storage mutation policies * remove demo storage exception * make storage hardening preview-safe * remove dead storage helper from baseline
1 parent 0ad6181 commit f521f8b

22 files changed

Lines changed: 462 additions & 46 deletions

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ Use local Studio, `psql`, or the hosted Supabase dashboard for browsing rows and
145145

146146
- Bucket configuration lives in `supabase/config.toml`
147147
- Local reset data lives in `supabase/seed.sql`
148-
- Local placeholder static assets live in `supabase/storage/static/`
148+
- Local bucket objects live in `supabase/storage/`
149+
- Local bucket policies are backfilled via SQL migrations so local uploads behave like the hosted project
150+
- `npm run supabase:reset` rebuilds the database and re-uploads seeded local media
151+
- `npm run supabase:seed-buckets` re-uploads local bucket media without resetting the database
149152

150153
Keep all local and preview data sanitized. Do not export or commit production data.
151154

docs/supabase-local-first.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ What this does:
3434
- A PR branch that changes `supabase/**` gets its own Supabase preview branch.
3535
- That preview branch runs migrations and bucket config from Git.
3636
- Seed data comes from `supabase/seed.sql`, not from production data.
37+
- Local bucket objects come from `supabase/storage/`, not from production storage.
3738

3839
### 2. Require the Supabase PR Check in GitHub
3940

@@ -78,6 +79,7 @@ npm install
7879
cp .env.example .env.local
7980
npm run supabase:start
8081
npm run supabase:reset
82+
npm run supabase:seed-buckets
8183
npm run supabase:env
8284
```
8385

@@ -117,10 +119,13 @@ The seed also creates:
117119
- 3 public listings
118120
- 1 chat thread
119121
- 2 chat messages
122+
- seeded profile avatars and listing photos in local Supabase Storage
120123
- a sample static bucket object at `static/promo-kit.zip`
121124

122125
The demo data is synthetic and safe to keep in Git.
123126

127+
When `NEXT_PUBLIC_SUPABASE_URL` points at `http://127.0.0.1:54331`, avatar and listing-photo uploads also go to the local Supabase buckets rather than production.
128+
124129
## Fresh Computer Setup
125130

126131
Use this when setting up Peels on a new machine.
@@ -158,9 +163,10 @@ npm run dev
158163
2. Make schema changes locally.
159164
3. Add or edit SQL migrations under `supabase/migrations/`.
160165
4. Rebuild from scratch with `npm run supabase:reset`.
161-
5. Run `npm run dev`.
162-
6. Test the flow locally.
163-
7. Commit app and migration changes together.
166+
5. Re-upload only local bucket media with `npm run supabase:seed-buckets` when you do not need a full DB reset.
167+
6. Run `npm run dev`.
168+
7. Test the flow locally.
169+
8. Commit app and migration changes together.
164170

165171
Use local Studio, `psql`, or the hosted dashboard for browsing rows. Use the CLI for schema lifecycle.
166172

next.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const uniqueStorageRemotePatterns = storageRemotePatterns.filter(
5757
)
5858
);
5959

60+
const shouldAllowLocalStorageImages =
61+
process.env.NEXT_PUBLIC_SUPABASE_URL?.includes("127.0.0.1") ||
62+
process.env.NEXT_PUBLIC_SUPABASE_URL?.includes("localhost") ||
63+
false;
64+
6065
/** @type {import('next').NextConfig} */
6166
const nextConfig = {
6267
// Configure `pageExtensions` to include markdown and MDX files
@@ -69,6 +74,7 @@ const nextConfig = {
6974
// https://nextjs.org/docs/app/api-reference/components/image#caching-behavior
7075
// Can be safetly increased as all user-generated imagery use uniques slugs
7176
minimumCacheTTL: 2678400, // 31 days (default value is `60`, i.e. one minute)
77+
dangerouslyAllowLocalIP: shouldAllowLocalStorageImages,
7278
// Define where remote images can be pulled from
7379
remotePatterns: uniqueStorageRemotePatterns,
7480
},

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"supabase:status": "supabase status",
1111
"supabase:env": "supabase status -o env",
1212
"supabase:reset": "supabase db reset",
13+
"supabase:seed-buckets": "supabase seed buckets --yes",
1314
"supabase:diff": "supabase db diff",
1415
"format": "prettier --write .",
1516
"format:check": "prettier --check ."

src/utils/listingUtils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ export function getListingAvatar(listing, user) {
2727

2828
// For demo listings, return path to public demo folder
2929
if (listing?.is_demo) {
30+
const demoAvatarFilename = listing?.avatar?.split("/").pop();
31+
3032
return {
3133
isDemo: true, // New flag to indicate demo image
32-
path: `/avatars/demo/${listing?.avatar}`,
34+
path: `/avatars/demo/${demoAvatarFilename}`,
3335
alt: `${listing?.name}’s avatar`,
3436
};
3537
}

supabase/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,19 @@ file_size_limit = "50MiB"
9191
public = true
9292
file_size_limit = "10MiB"
9393
allowed_mime_types = ["image/png", "image/jpeg", "image/webp"]
94+
objects_path = "./storage/avatars"
9495

9596
[storage.buckets.listing_avatars]
9697
public = true
9798
file_size_limit = "10MiB"
9899
allowed_mime_types = ["image/png", "image/jpeg", "image/webp"]
100+
objects_path = "./storage/listing_avatars"
99101

100102
[storage.buckets.listing_photos]
101103
public = true
102104
file_size_limit = "50MiB"
103105
allowed_mime_types = ["image/png", "image/jpeg", "image/webp"]
106+
objects_path = "./storage/listing_photos"
104107

105108
[storage.buckets.static]
106109
public = true

supabase/migrations/20251026060000_baseline_schema.sql

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -147,34 +147,6 @@ $$;
147147
ALTER FUNCTION "public"."count_verified_users_with_listings"() OWNER TO "postgres";
148148

149149

150-
CREATE OR REPLACE FUNCTION "public"."delete_storage_object"("bucket" "text", "object" "text") RETURNS "record"
151-
LANGUAGE "plpgsql" SECURITY DEFINER
152-
AS $$
153-
DECLARE
154-
-- Local bootstrap should call the local Supabase gateway, not production.
155-
project_url text := 'http://kong:8000';
156-
service_role_key text := 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU';
157-
url text := project_url || '/storage/v1/object/' || bucket || '/' || object;
158-
response record; -- Use a record to capture the response
159-
BEGIN
160-
SELECT
161-
result.status::int, result.content::text
162-
INTO response
163-
FROM extensions.http((
164-
'DELETE',
165-
url,
166-
ARRAY[extensions.http_header('authorization', 'Bearer ' || service_role_key)],
167-
NULL,
168-
NULL)::extensions.http_request) AS result;
169-
170-
RETURN (response.status, response.content); -- Return the status and content as a record
171-
END;
172-
$$;
173-
174-
175-
ALTER FUNCTION "public"."delete_storage_object"("bucket" "text", "object" "text") OWNER TO "postgres";
176-
177-
178150
CREATE OR REPLACE FUNCTION "public"."generate_unique_slug"() RETURNS "text"
179151
LANGUAGE "plpgsql"
180152
AS $$
@@ -811,12 +783,6 @@ GRANT ALL ON FUNCTION "public"."count_verified_users_with_listings"() TO "servic
811783

812784

813785

814-
GRANT ALL ON FUNCTION "public"."delete_storage_object"("bucket" "text", "object" "text") TO "anon";
815-
GRANT ALL ON FUNCTION "public"."delete_storage_object"("bucket" "text", "object" "text") TO "authenticated";
816-
GRANT ALL ON FUNCTION "public"."delete_storage_object"("bucket" "text", "object" "text") TO "service_role";
817-
818-
819-
820786
GRANT ALL ON FUNCTION "public"."generate_unique_slug"() TO "anon";
821787
GRANT ALL ON FUNCTION "public"."generate_unique_slug"() TO "authenticated";
822788
GRANT ALL ON FUNCTION "public"."generate_unique_slug"() TO "service_role";
@@ -944,4 +910,3 @@ ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT SELECT,INS
944910
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT SELECT,INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLES TO "authenticated";
945911
ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT SELECT,INSERT,REFERENCES,DELETE,TRIGGER,TRUNCATE,UPDATE ON TABLES TO "service_role";
946912

947-
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
begin;
2+
3+
-- Backfill Storage RLS policies that already exist in the hosted project but
4+
-- were not captured in the initial public-schema baseline pull.
5+
6+
do $$
7+
begin
8+
if not exists (
9+
select 1
10+
from pg_policies
11+
where schemaname = 'storage'
12+
and tablename = 'objects'
13+
and policyname = 'Allow authenticated changes 1oj01fe_0'
14+
) then
15+
execute $policy$
16+
create policy "Allow authenticated changes 1oj01fe_0"
17+
on storage.objects
18+
for update
19+
to authenticated
20+
using ((bucket_id = 'avatars') and (auth.role() = 'authenticated'))
21+
$policy$;
22+
end if;
23+
24+
if not exists (
25+
select 1
26+
from pg_policies
27+
where schemaname = 'storage'
28+
and tablename = 'objects'
29+
and policyname = 'Allow authenticated changes 1oj01fe_1'
30+
) then
31+
execute $policy$
32+
create policy "Allow authenticated changes 1oj01fe_1"
33+
on storage.objects
34+
for insert
35+
to authenticated
36+
with check ((bucket_id = 'avatars') and (auth.role() = 'authenticated'))
37+
$policy$;
38+
end if;
39+
40+
if not exists (
41+
select 1
42+
from pg_policies
43+
where schemaname = 'storage'
44+
and tablename = 'objects'
45+
and policyname = 'Allow authenticated deletes 1oj01fe_0'
46+
) then
47+
execute $policy$
48+
create policy "Allow authenticated deletes 1oj01fe_0"
49+
on storage.objects
50+
for delete
51+
to authenticated
52+
using ((bucket_id = 'avatars') and (auth.role() = 'authenticated'))
53+
$policy$;
54+
end if;
55+
56+
if not exists (
57+
select 1
58+
from pg_policies
59+
where schemaname = 'storage'
60+
and tablename = 'objects'
61+
and policyname = 'Allow authenticated list 1oj01fe_0'
62+
) then
63+
execute $policy$
64+
create policy "Allow authenticated list 1oj01fe_0"
65+
on storage.objects
66+
for select
67+
to authenticated
68+
using (bucket_id = 'avatars')
69+
$policy$;
70+
end if;
71+
72+
if not exists (
73+
select 1
74+
from pg_policies
75+
where schemaname = 'storage'
76+
and tablename = 'objects'
77+
and policyname = 'Anyone can view listing avatars'
78+
) then
79+
execute $policy$
80+
create policy "Anyone can view listing avatars"
81+
on storage.objects
82+
for select
83+
using (bucket_id = 'listing_avatars')
84+
$policy$;
85+
end if;
86+
87+
if not exists (
88+
select 1
89+
from pg_policies
90+
where schemaname = 'storage'
91+
and tablename = 'objects'
92+
and policyname = 'Anyone can view listing photos'
93+
) then
94+
execute $policy$
95+
create policy "Anyone can view listing photos"
96+
on storage.objects
97+
for select
98+
using (bucket_id = 'listing_photos')
99+
$policy$;
100+
end if;
101+
102+
if not exists (
103+
select 1
104+
from pg_policies
105+
where schemaname = 'storage'
106+
and tablename = 'objects'
107+
and policyname = 'Users can delete their own listing avatars'
108+
) then
109+
execute $policy$
110+
create policy "Users can delete their own listing avatars"
111+
on storage.objects
112+
for delete
113+
to authenticated
114+
using (bucket_id = 'listing_avatars')
115+
$policy$;
116+
end if;
117+
118+
if not exists (
119+
select 1
120+
from pg_policies
121+
where schemaname = 'storage'
122+
and tablename = 'objects'
123+
and policyname = 'Users can delete their own listing photos'
124+
) then
125+
execute $policy$
126+
create policy "Users can delete their own listing photos"
127+
on storage.objects
128+
for delete
129+
to authenticated
130+
using (bucket_id = 'listing_photos')
131+
$policy$;
132+
end if;
133+
134+
if not exists (
135+
select 1
136+
from pg_policies
137+
where schemaname = 'storage'
138+
and tablename = 'objects'
139+
and policyname = 'Users can update their own listing avatars'
140+
) then
141+
execute $policy$
142+
create policy "Users can update their own listing avatars"
143+
on storage.objects
144+
for update
145+
to authenticated
146+
using (bucket_id = 'listing_avatars')
147+
with check (auth.role() = 'authenticated')
148+
$policy$;
149+
end if;
150+
151+
if not exists (
152+
select 1
153+
from pg_policies
154+
where schemaname = 'storage'
155+
and tablename = 'objects'
156+
and policyname = 'Users can update their own listing photos'
157+
) then
158+
execute $policy$
159+
create policy "Users can update their own listing photos"
160+
on storage.objects
161+
for update
162+
to authenticated
163+
using (bucket_id = 'listing_photos')
164+
with check (auth.role() = 'authenticated')
165+
$policy$;
166+
end if;
167+
168+
if not exists (
169+
select 1
170+
from pg_policies
171+
where schemaname = 'storage'
172+
and tablename = 'objects'
173+
and policyname = 'Users can upload their own listing avatars'
174+
) then
175+
execute $policy$
176+
create policy "Users can upload their own listing avatars"
177+
on storage.objects
178+
for insert
179+
to authenticated
180+
with check ((bucket_id = 'listing_avatars') and (auth.role() = 'authenticated'))
181+
$policy$;
182+
end if;
183+
184+
if not exists (
185+
select 1
186+
from pg_policies
187+
where schemaname = 'storage'
188+
and tablename = 'objects'
189+
and policyname = 'Users can upload their own listing photos'
190+
) then
191+
execute $policy$
192+
create policy "Users can upload their own listing photos"
193+
on storage.objects
194+
for insert
195+
to authenticated
196+
with check ((bucket_id = 'listing_photos') and (auth.role() = 'authenticated'))
197+
$policy$;
198+
end if;
199+
end
200+
$$;
201+
202+
commit;

0 commit comments

Comments
 (0)