Skip to main content
RapidDev - Software Development Agency
App Featuresmedia-content16 min read

How to Add a Media Gallery to Your App (Copy-Paste Prompts Included)

A media gallery needs four pieces: a file upload layer with MIME/size validation, Supabase Storage for file hosting, a metadata table for searching and ordering, and a grid UI with lightbox and video playback. With Lovable or FlutterFlow you can ship a working gallery in 3–6 hours for $0/month at small scale. Storage costs only appear past 1GB of uploads — roughly 1,000 active users.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

media-content

Build with AI

3–6 hours with Lovable or FlutterFlow

Custom build

1–2 weeks custom dev

Running cost

$0/mo up to ~1 GB storage · $25–60/mo at 10K users

Works on

Mobile

Everything it takes to ship a Media Gallery — parts, prompts, and real costs.

TL;DR

A media gallery needs four pieces: a file upload layer with MIME/size validation, Supabase Storage for file hosting, a metadata table for searching and ordering, and a grid UI with lightbox and video playback. With Lovable or FlutterFlow you can ship a working gallery in 3–6 hours for $0/month at small scale. Storage costs only appear past 1GB of uploads — roughly 1,000 active users.

What a Media Gallery Feature Actually Is

A media gallery lets users upload, browse, and manage their own photos and videos inside your app — not just link to external content. The upload layer handles MIME validation and byte-level progress; Supabase Storage hosts the files behind per-user access policies; a metadata table records captions, types, and sort order; and the grid UI handles thumbnail generation, lightbox overlay, and video playback. The product decisions that matter most are the file size cap you enforce, whether users can caption or organize items, and whether privacy is enforced at the row level or left open. Getting storage RLS wrong is the most common reason galleries accidentally expose one user's photos to another.

What users consider table stakes in 2026

  • Pinch-to-zoom on full-screen images with swipe navigation between items in the lightbox
  • Grid and list toggle so power users can scan their content faster
  • Upload progress bar per file so users know their photo is not lost mid-upload
  • Video items show a play icon overlay on the thumbnail and launch native playback controls
  • Empty-state illustration with a clear call-to-action upload button when the gallery has no items yet
  • Pull-to-refresh to sync new uploads made on another device without a full app restart

Anatomy of the Feature

Six components across four layers. AI tools scaffold the grid and upload widget reliably — the storage policy and video thumbnail layers are where builds stall.

Layers:UIDataBackendService

Grid / Lightbox UI

UI

Renders a staggered two- or three-column grid using React Native FlatList (or FlutterFlow's GridView widget). Tapping any item opens react-native-image-viewing as a full-screen lightbox with swipe navigation between all gallery items.

Note: FlatList without initialNumToRender and windowSize props causes visible jank past 50 items — specify performance props in your prompt.

File upload layer

Backend

Accepts file objects from the device picker, validates MIME type (image/jpeg, image/png, video/mp4) and byte size before uploading to Supabase Storage via supabase-js. The onUploadProgress callback populates a per-file progress bar.

Note: Supabase Storage signed URLs are required for private buckets. The upload layer must request a signed upload URL from your Supabase Edge Function rather than using the anon key client-side.

Media metadata store

Data

A PostgreSQL table (media_items) in Supabase storing user_id, storage_path, mime_type, file_size, caption, and created_at. Querying this table drives the grid — media files themselves are fetched by constructing signed download URLs from storage_path.

Note: Storing storage_path (not the full URL) future-proofs you: if you move buckets or add a CDN, only one column needs updating.

Video player

UI

react-native-video (in React Native apps) or FlutterFlow's built-in VideoPlayer widget renders video items. Set autoplay=false and require a user tap on the play overlay so videos don't fire unexpectedly in a scroll list.

Note: Lovable's preview iframe blocks autoplay and poster attribute loading — test video playback only on the published URL.

Image optimizer

Service

Supabase Storage's built-in image transformation API generates thumbnails on the fly: append ?width=400&quality=80 to any storage URL. No separate CDN or transcoding service is needed at under 10,000 users — Supabase Storage serves via its own global CDN.

Note: Transformation only works on images, not video. Generate video thumbnails by extracting a frame at upload time using a Supabase Edge Function.

RLS enforcement layer

Backend

Supabase Row Level Security policies on both the media_items table and the Storage bucket ensure each user can only read and write their own files. Without explicit policies, Supabase denies all access by default — but AI tools sometimes skip this step and leave buckets public.

Note: Set bucket visibility to Private, then add Storage policies scoped to auth.uid() = user_id in addition to table-level RLS.

The data model

One table tracks media metadata with row-level security; run this in the Supabase SQL editor to set it up:

schema.sql
1create table public.media_items (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete cascade not null,
4 storage_path text not null,
5 mime_type text not null,
6 file_size bigint not null,
7 caption text,
8 created_at timestamptz not null default now()
9);
10
11alter table public.media_items enable row level security;
12
13create policy "Users can view own media"
14 on public.media_items for select
15 using (auth.uid() = user_id);
16
17create policy "Users can insert own media"
18 on public.media_items for insert
19 with check (auth.uid() = user_id);
20
21create policy "Users can delete own media"
22 on public.media_items for delete
23 using (auth.uid() = user_id);
24
25create index media_items_user_created_idx
26 on public.media_items (user_id, created_at desc);

Heads up: The descending index on (user_id, created_at) keeps the gallery grid query fast as individual users accumulate hundreds of items. Storage bucket policies must be added separately in Supabase Dashboard → Storage → Policies: INSERT and SELECT both scoped to auth.uid() = user_id.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Native iOS + AndroidFit for this feature:

Best path for mobile — GridView, VideoPlayer widget, and CachedNetworkImage all ship as built-in FlutterFlow components. Supabase Storage is natively supported alongside Firebase Storage.

Step by step

  1. 1Add a new page in FlutterFlow with a GridView widget (2 columns, cross-axis count); set each cell to a Stack with a CachedNetworkImage and a conditional play-icon overlay for video items
  2. 2Add a FloatingActionButton that opens a file picker action; in the Action editor chain: Pick Media → Upload to Supabase Storage → Insert Row into media_items table
  3. 3Wire the pull-to-refresh widget wrapping the GridView to re-query the media_items table filtered by auth.uid()
  4. 4Add a lightbox page: GridView item tap navigates to a full-screen image/video page passing the storage_path as a route parameter
  5. 5In Settings → Permissions, enable camera and photo library permissions with descriptive iOS usage strings; test on a real device via FlutterFlow's Test on Device feature

Where this path bites

  • FlutterFlow Pro ($70/mo) is required for code export and for custom Supabase queries beyond simple CRUD
  • CachedNetworkImage caches the signed URL string, not the image bytes — signed URLs that expire before the TTL causes broken images; set expiry to 86400 seconds
  • Free tier restricts advanced Supabase Storage policies; configure bucket RLS in the Supabase Dashboard directly

Third-party services you'll need

The gallery itself needs only Supabase. External services are optional additions for scale or AI features:

ServiceWhat it doesFree tierPaid from
Supabase StorageHosts uploaded photos and videos behind per-user access policies; serves thumbnails via built-in image transformation API1 GB storage included on Supabase Free plan$0.021/GB/mo storage + $0.09/GB/mo bandwidth (approx)
Supabase DatabaseStores media_items metadata table with RLS; drives gallery grid queries500 MB included on Supabase Free plan$25/mo for Supabase Pro (8 GB DB, unlimited bandwidth)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier covers up to 1 GB storage and 500 MB DB — typical for 100 users uploading a handful of photos each. Supabase Storage CDN bandwidth is free on the Free plan.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Video thumbnails are blank in the Lovable preview

Symptom: Lovable's preview iframe enforces a restrictive content security policy that blocks video autoplay and poster attribute loading. The video element renders but its thumbnail frame never loads, making it look like a broken feature.

Fix: Click Publish → Update and open the live URL on a real mobile browser. Poster images and autoplay restrictions are lifted in the production deployment. Do all video testing outside the preview iframe.

Supabase Storage bucket left as public, exposing all user media

Symptom: AI tools frequently generate Supabase Storage code that creates a public bucket or omits Storage-level access policies entirely. Anyone with a valid storage URL can then view any user's photo — a serious privacy failure that is not caught by table-level RLS alone.

Fix: In Supabase Dashboard → Storage → Policies, set the bucket to Private and add INSERT and SELECT policies scoped to (auth.uid() = user_id). Table RLS and Storage bucket policies are separate systems — both are required.

FlatList performance degrades at 100+ items

Symptom: AI-generated FlatList code typically omits virtualisation tuning props: no initialNumToRender, no maxToRenderPerBatch, no windowSize. At 100+ items, every scroll event triggers a burst of off-screen renders, causing dropped frames and memory growth.

Fix: Add initialNumToRender={12}, maxToRenderPerBatch={12}, windowSize={5}, and a keyExtractor returning a unique string (item.id). Specify these props explicitly in your prompt — the AI will include them only if asked.

FlutterFlow's CachedNetworkImage caches stale signed URLs

Symptom: Supabase signed URLs expire after their TTL (default 1 hour). FlutterFlow's CachedNetworkImage widget caches the URL string itself, so after the TTL elapses the cached URL returns a 403, and the image shows a broken icon rather than refetching.

Fix: Set signed URL expiry to 86400 seconds (24 hours) in your Supabase Storage createSignedUrl call. Add an AppState listener that triggers a signed URL refresh when the app resumes from background, replacing cached entries.

Duplicate media items created on upload retry

Symptom: When a user taps the upload button, sees a slow progress bar, and taps again, two upload requests fire in parallel. Both complete successfully, creating two media_items rows for the same file. The upload progress handler firing onUploadProgress does not prevent re-tap.

Fix: Track upload state in a local state machine with four values: idle, uploading, done, and error. Disable the upload button (pointer-events: none + visual indicator) during the uploading state. Only return to idle on success or explicit user cancel — not on progress callback.

Best practices

1

Enforce MIME type and file size validation before the upload starts — reject files larger than 10 MB or with unsupported types client-side, not in a server error response

2

Use Supabase Storage image transformations (?width=400&quality=80) for grid thumbnails instead of loading original files — a 5 MB JPEG scaled to 400px saves 90% of bandwidth per grid cell

3

Store storage_path in your metadata table, not the full signed URL — signed URLs expire and change; constructing them from a stable path keeps your data clean

4

Set both table-level RLS policies and Storage bucket-level policies — they are independent systems and omitting either one creates a security gap

5

Show a loading skeleton grid on first render instead of a spinner — skeletons reduce perceived wait time and prevent layout shift when the real grid appears

6

Add a delete confirmation dialog before removing any media item — accidental deletion of photos is one of the most common user complaints in early app reviews

7

Test upload and video playback on a real mobile device from the start — camera permissions, file pickers, and video rendering all behave differently than desktop browser simulations

8

Set signed URL expiry to 86400 seconds for cached images — short expiry causes broken images in FlutterFlow and any caching layer

When You Need Custom Development

AI tools handle personal galleries and user media libraries well. These requirements push beyond what Lovable or FlutterFlow can reliably deliver:

  • App needs AI-powered auto-tagging, face grouping, or object recognition using Google ML Kit Vision — this requires a server-side processing pipeline triggered on every upload
  • Gallery must serve as a product showcase with custom watermarking, logo overlays, or brand-consistent output — server-side image compositing (sharp) is needed
  • Video must be transcoded server-side: format normalization, resolution capping, or thumbnail extraction at a specific frame — requires a worker process outside Supabase Edge Functions
  • Compliance requirements mandate encrypted storage beyond Supabase defaults, audit logs of every file access, or data residency in a specific region

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I let users upload both photos and videos in the same gallery?

Yes. Accept image/jpeg, image/png, and video/mp4 in your MIME type allowlist and store the mime_type column in media_items. Your UI then conditionally renders an image component or a video player based on that value. Specify all three MIME types explicitly in your prompt — AI tools default to images only.

How do I stop users from seeing each other's media?

You need two layers of access control: Row Level Security on the media_items table (policy: auth.uid() = user_id) AND a Private Storage bucket with bucket-level policies for the same condition. Table RLS alone does not protect the raw file URLs in Storage — both systems must be configured.

What file size limit should I set?

10 MB per file is a safe default that covers most phone photos and short video clips while keeping storage costs predictable. For video-heavy apps raise it to 50–100 MB but add server-side validation in a Supabase Edge Function — client-side validation alone can be bypassed.

Does this work offline — can users view cached photos without internet?

By default no — images are fetched from Supabase Storage URLs. FlutterFlow's CachedNetworkImage stores images in device cache, so recently viewed photos load offline. For full offline support (browsing all owned media without a connection) you need a custom local database sync, which is beyond what AI tools generate out of the box.

How do I add captions or descriptions to media items?

The media_items table includes a caption text column. In the UI, add a text input below the upload button (or in the lightbox view) that writes to that column via a Supabase update call. Mention caption editing explicitly in your prompt — otherwise the AI generates the column but no UI to edit it.

Can I add search or filter to the gallery?

Yes — filtering by mime_type (photos vs videos), date range, or keyword search on caption are all Supabase query parameters. Add a filter bar above the grid and pass the selected filters to your Supabase .select().eq().ilike() chain. For full-text search across captions, enable the pg_trgm extension in Supabase and add a GIN index on the caption column.

How much storage will 1,000 active users consume?

Assuming each user uploads an average of 20 items at ~500 KB each (compressed phone photos), that is 10 GB — costing roughly $0.21/month in Supabase Storage fees. Video uploads change this significantly: 20 short videos at 5 MB each would be 100 GB, or about $2.10/month. Enforce file size limits early to keep this number predictable.

Can I generate automatic thumbnails for videos?

Supabase Storage's image transformation API only works on images, not video files. For video thumbnails, trigger a Supabase Edge Function on upload that uses ffmpeg (via a Deno subprocess or an external worker) to extract a frame at 1 second, uploads it as a JPEG to Storage, and writes the thumbnail_url back to the media_items row. Specify this in your prompt if you want AI tools to scaffold the Edge Function.

RapidDev

Need this feature production-ready?

RapidDev builds a media gallery into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.