# Supabase in Practice: Auth, Database, and Storage

Hello everyone! When I started building my [Year Soundtrack Generator](https://soundtrack.codeanding.com/en), 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](/from-life-moments-to-songs), 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:**

1. **Re-generation**: If a track generation fails, I can retry without losing the user's original moment description
2. **Partial success**: 3 out of 5 tracks can succeed while 2 fail
3. **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](https://astro.build/) 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](https://supabase.com/docs/guides/api/api-keys). 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](https://github.com/orgs/supabase/discussions/29260).

### Row Level Security (RLS)

RLS is what makes the publishable key safe. It controls what each user can access at the database level:

```sql
-- 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:

```typescript
// 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](https://supabase.com/docs/guides/auth/server-side/creating-a-client) for the full implementation.

### Middleware for Protected Routes

To protect routes, I use Astro middleware:

```typescript
// 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:

```typescript
// 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:

```sql
-- 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:

```typescript
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](https://supabase.com/docs/guides/api/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](https://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](/from-life-moments-to-songs) covers how I use ElevenLabs Music API to generate personalized songs from life moments.*
