Supabase in Practice: Auth, Database, and Storage

Hello everyone! When I started building my Year Soundtrack Generator, I needed three things: user authentication, a database, and file storage for audio. Instead of juggling multiple services, I chose Supabase.
In my previous post, I covered how I use ElevenLabs to generate personalized music. Today, let's dive into the backend.
What is Supabase?
Supabase is an open-source Firebase alternative that provides backend services for developers. I chose it for this project because:
- Free tier: Generous limits for side projects (500MB database, 1GB storage, 50K monthly active users)
- Framework support: Official SDKs for JavaScript, Flutter, Python, and integrations with Next.js, Astro, SvelteKit, etc.
- All-in-one: Database, auth, and storage in one platform - fewer services to manage
For this project, I used three of its core features:
- Database: PostgreSQL with real-time subscriptions and auto-generated APIs
- Authentication: Email/password, magic links, and OAuth providers
- Storage: File storage with CDN delivery, perfect for serving audio files
Database Design: Thinking in Layers
Before writing any code, I had to think about how to structure my data. The key insight was separating user input from AI output.
Here's what I came up with:
┌──────────────────────────────────┐
│ profiles (credits, settings) │
└────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────┐
│ soundtracks (title, year) │
└────────────────┬─────────────────┘
│
┌────────┴────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ moments │ │ tracks │
│ (user input) │─▶│ (AI output) │
└───────────────┘ └───────────────┘
Why this separation matters:
- Re-generation: If a track generation fails, I can retry without losing the user's original moment description
- Partial success: 3 out of 5 tracks can succeed while 2 fail
- Audit trail: I store the exact prompt used for each track, so I can debug or reproduce results
Pro tip: Supabase can generate TypeScript types from your schema (similar to what Prisma does). It's been really helpful for catching errors early.
Authentication with Magic Links
The frontend is built with Astro using SSR mode. For Supabase auth in SSR frameworks, you need cookie-based sessions. The @supabase/ssr package handles this.
A Note on API Keys
Supabase recently introduced a new API key system. You'll see two types:
- Publishable key (
sb_publishable_...): Safe for client-side code. Security comes from Row Level Security (RLS) policies, not from hiding the key. - Secret key (
sb_secret_...): Server-side only. Bypasses RLS - never expose this in client code.
The older anon and service_role keys still work but are being phased out.
Row Level Security (RLS)
RLS is what makes the publishable key safe. It controls what each user can access at the database level:
-- Users can only read their own soundtracks
create policy "Users read own soundtracks"
on soundtracks for select
using (auth.uid() = user_id);
Without RLS enabled, your data is exposed to anyone with the publishable key. Supabase shows security alerts in your dashboard if tables are missing RLS policies - pay attention to them!
Setting Up the Clients
For browser-side:
// src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr';
export const supabase = createBrowserClient<Database>(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY
);
For server-side (Astro), you need to manage cookies manually with getAll() and setAll() handlers. See the @supabase/ssr documentation for the full implementation.
Middleware for Protected Routes
To protect routes, I use Astro middleware:
// src/middleware.ts
export const onRequest = async ({ locals, url, cookies, redirect }) => {
const supabase = createServerSupabaseClient(cookies);
const { data: { user } } = await supabase.auth.getUser();
const protectedRoutes = ['/create', '/profile'];
const isProtected = protectedRoutes.some(route =>
url.pathname.startsWith(route)
);
if (isProtected && !user) {
return redirect(`/auth/login?redirect=${url.pathname}`);
}
locals.user = user;
};
File Storage for Audio
After ElevenLabs generates an MP3, I upload it directly to Supabase Storage:
// Upload to Supabase Storage
const fileName = `${user.id}/${soundtrackId}/track_${orderIndex}.mp3`;
const { error: uploadError } = await supabase.storage
.from('soundtracks')
.upload(fileName, audioBuffer, {
contentType: 'audio/mpeg',
upsert: true,
});
// Get public URL for playback
const { data: { publicUrl } } = supabase.storage
.from('soundtracks')
.getPublicUrl(fileName);
The path structure {user_id}/{soundtrack_id}/track_{index}.mp3 makes it easy to:
- Find all tracks for a soundtrack
- Clean up when a user deletes their data
- Set bucket policies per user
Bucket Policies
Storage buckets also need policies. I configured mine to allow public reads (so anyone can play shared soundtracks) but restrict writes to authenticated users:
-- Allow public read access
create policy "Public read access"
on storage.objects for select
using (bucket_id = 'soundtracks');
-- Allow authenticated users to upload
create policy "Authenticated users can upload"
on storage.objects for insert
with check (bucket_id = 'soundtracks' and auth.role() = 'authenticated');
Rate Limiting
Supabase doesn't include built-in rate limiting for the Data API. Common options:
- Cloudflare: Free rate limiting rules if your domain is proxied through them
- Redis (Upstash): Best for high-traffic apps with atomic operations
- Database table: Simple approach for smaller projects
For this project, I use Cloudflare at the DNS level (my domain is proxied through them), plus a simple usage_logs table for app-level tracking:
async function checkRateLimit(userId: string, action: string) {
const windowMs = 60 * 60 * 1000; // 1 hour
const maxRequests = 10; // 10 tracks per hour
const since = new Date(Date.now() - windowMs);
const { count } = await supabase
.from('usage_logs')
.select('*', { count: 'exact' })
.eq('user_id', userId)
.eq('action', action)
.gte('created_at', since.toISOString());
return { allowed: (count || 0) < maxRequests };
}
For more options, see Supabase's guide on securing your API.
That's how I integrated auth, database, and storage with Supabase. For a side project, having everything in one platform with a free tier made it easy to go from idea to deployed app.
You can try the app at soundtrack.codeanding.com/en. Have you used Supabase with Astro or another SSR framework? I'd love to hear about your experience!
This is part 2 of a 2-part series. Part 1 covers how I use ElevenLabs Music API to generate personalized songs from life moments.



