antrix.dev
Ship Your Product · $79

Database with Supabase

Set up your database, authentication, and file storage with Supabase. PostgreSQL under the hood, generous free tier.

What it does

Supabaseis your database, authentication, file storage, and real-time subscriptions — all in one service. It's PostgreSQL under the hood, which means you get a real, battle-tested relational database with full SQL support. Not a proprietary abstraction that locks you in.

The dashboard lets you manage everything visually — create tables, write queries, manage users, configure storage buckets, and set up security policies. The client library gives your app direct database access with built-in auth handling. You get a complete backend without writing a single line of backend code.

Why we use it

Supabase is the Firebase alternative that doesn't lock you in. Your data lives in a standard PostgreSQL database that you can export, migrate, or connect to from any tool. If you ever outgrow Supabase, you take your data with you.

The free tier is genuinely generous: 500MB database storage, 1GB file storage, 50,000 monthly active users, and unlimited API requests. For most solo products, you'll run for months (or longer) without paying a cent.

Row Level Securityis the key feature that makes this stack work. RLS lets you write security rules directly in the database — so you can safely query from the client without building an entire API layer. The anon key in your client code is safe to expose because RLS prevents unauthorized access at the database level.

Setup checklist

1

Create a Supabase account

Go to supabase.com and sign up with GitHub. Click New Project, give it a name, and set a strong database password— save this somewhere safe, you'll need it if you ever connect directly via psql or a database tool.

Choose a region close to your users. US East (Virginia) works well for North American audiences. EU West (Ireland) for European. Pick the same region as your Vercel deployment for the lowest latency between your app and your database.

2

Get your project credentials

Go to Settings > API in your project dashboard. You need two values: NEXT_PUBLIC_SUPABASE_URL (your project URL, looks like https://abcdefg.supabase.co) and NEXT_PUBLIC_SUPABASE_ANON_KEY (your public anonymous key).

The anon key is safe to expose in client-side code — it's designed to be public. It only grants access that your RLS policies allow. The service_role key, on the other hand, bypasses all RLS. That one is a secret and must never appear in client code.

Add both values to your .env.local for local development and to your Vercel environment variables for production. The NEXT_PUBLIC_ prefix is required for Next.js to make them available in client-side code.

3

Install the Supabase client

Run npm install @supabase/supabase-js. Then create a client file that you'll import throughout your app. This client handles all communication with your Supabase project — database queries, auth, storage, and real-time subscriptions.

4

Design your database schema

Use the Table Editor in the dashboard for quick visual prototyping, or write SQL directly in the SQL Editor. Start with your core tables — the 3-5 tables that represent the fundamental data your app needs.

Think about relationships: a user has many projects, a project has many tasks. Use foreign keys to enforce these relationships. Add a user_idcolumn to any table where you need to know who owns the data — you'll need this for RLS policies.

Don't over-design. You can always add columns and tables later. For an MVP, simpler is better. You don't need junction tables, polymorphic associations, or complex inheritance hierarchies. You need tables that store the data your users create.

5

Set up Row Level Security (RLS)

This is non-negotiable. Enable RLS on every single table. Without it, anyone who has your anon key (which is in your client-side JavaScript, so everyone) can read and write every row in your database.

For each table, create policies that answer these questions: Who can read rows? Who can insert new rows? Who can update existing rows? Who can delete rows? The most common pattern is: users can only access their own data.

A basic policy looks like: auth.uid() = user_id. This checks that the logged-in user's ID matches the user_id column on the row. Apply this to SELECT, INSERT, UPDATE, and DELETE operations. Test every policy in the SQL Editor by running queries as different users.

For public-facing data (like a blog or product listings), create a SELECT policy that allows access to everyone: true as the condition. But keep INSERT, UPDATE, and DELETE restricted to the data owner or admin users.

6

Create your first migration

Install the Supabase CLI and initialize your project: npx supabase init. Then capture your current schema as a migration file: npx supabase db diff --schema public -f create_tables. This generates a SQL file in supabase/migrations/ that can recreate your schema from scratch.

Migrations are version-controlled SQL. They're the source of truth for your database structure. When you change your schema, create a new migration — don't edit old ones. This ensures your changes are reproducible across local development, staging, and production.

Commit your migration files to Git. They should be treated like code — reviewed, versioned, and deployed through your normal workflow.

7

Set up local development

Run npx supabase start to spin up a local Supabase instance. This requires Docker— install Docker Desktop if you don't have it. The local instance includes a Postgres database, Auth server, Storage, and a local dashboard at localhost:54323.

Your local instance starts with the same schema as your migrations. Use npx supabase db reset to wipe local data and re-run all migrations from scratch. This is useful when you want a clean slate or when testing new migrations.

Develop against local, deploy to production. This keeps your production data safe and lets you experiment freely. Update your .env.local to point at the local instance URLs that supabase start outputs.

8

Connect to your Next.js app

For client-side operations (reading public data, user-specific queries with RLS), use the Supabase client you created with the anon key. RLS ensures users only see their own data.

For server-sideoperations (API routes, Server Actions) that need to bypass RLS — like admin operations or processing webhooks — create a separate client using the service_role key. Store the service role key in SUPABASE_SERVICE_ROLE_KEY(no NEXT_PUBLIC_ prefix) so it's only available server-side.

Never import or use the service role client in any component or code that runs in the browser. If your service role key leaks, anyone can read and write your entire database with no restrictions.

9

Set up authentication (if needed)

Supabase Auth supports multiple providers out of the box: email/password, magic links, Google OAuth, GitHub OAuth, and more. Enable the ones you need in Authentication > Providers in your dashboard.

The client library gives you supabase.auth.signUp(), signInWithPassword(), and signInWithOAuth(). For OAuth providers, you'll need to create an app in the provider's developer console (Google Cloud Console, GitHub Settings) and add the client ID and secret to your Supabase dashboard.

Configure your redirect URLsin Authentication > URL Configuration. Set your site URL to your production domain and add any additional redirect URLs for local development (like http://localhost:3000). Without correct redirect URLs, OAuth flows will fail silently.

10

Set up storage (if needed)

Go to Storage in your dashboard and create a new bucket. Buckets can be public (anyone can download files via URL) or private (requires authentication). Use public for profile images and assets. Use private for user documents and uploads.

Upload files from your app using supabase.storage.from('bucket-name').upload('path/file.jpg', file). Storage has its own security policies, just like RLS for tables. Create policies to control who can upload, download, and delete files in each bucket.

For images, Supabase provides automatic transformations — resize, crop, and format conversion via URL parameters. This means you can upload one high-resolution image and serve optimized versions without any processing on your end.

11

Deploy migrations to production

Link your local project to your remote Supabase project: npx supabase link --project-ref YOUR_PROJECT_REF. You'll find your project ref in Settings > General in the dashboard.

Push your migrations to production: npx supabase db push. This applies any new migration files that haven't been run on the remote database yet. Always test migrations locally first using npx supabase db reset before pushing to production.

Make this part of your deploy workflow: write the migration locally, test it with a reset, commit the migration file, push to GitHub, then run db push to apply it to production. Schema changes should never be made by clicking around in the production dashboard.

Sample client setup

// lib/supabase.ts import { createClient } from "@supabase/supabase-js"; import type { Database } from "@/types/supabase"; export const supabase = createClient<Database>( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! );

Generate your Database type with: npx supabase gen types typescript --project-id YOUR_REF > types/supabase.ts

Pro tips

Use Supabase's TypeScript generation to get type-safe database queries. Run npx supabase gen types typescript --linked > types/supabase.ts to generate types from your schema. Pass them to createClient<Database>() and your queries will autocomplete column names and catch type errors at build time.

The SQL Editorin the dashboard is your best debugging tool. When a query isn't returning what you expect, run it directly in the SQL Editor to see the raw results. You can also test RLS policies by switching between user contexts.

For complex queries that don't map cleanly to the client library's API, create a database function (stored procedure) and call it with supabase.rpc('function_name', params). This keeps complex logic in SQL where it's fast, and gives you a clean interface from your app.

Set up database webhooks or Postgres triggers for side effects. When a new user signs up, automatically create their profile row. When an order is placed, update inventory counts. Let the database handle cascading logic instead of coordinating it in application code.

AI prompt to get started

I'm building [describe your app]. Help me design the Supabase database schema. I need tables for [describe your data]. For each table, give me: the CREATE TABLE SQL, RLS policies for authenticated users, and the TypeScript types I should use in my app.

Mistakes to avoid

  • Skipping Row Level Security — this is a security disaster waiting to happen. Enable RLS on every table, no exceptions. Without it, your entire database is publicly readable and writable
  • Using the service role key in client-side code — this bypasses all RLS and gives full database access. Only use it in server-side code (API routes, Server Actions). If it leaks, your data is exposed
  • Not using migrations — clicking around in the dashboard works until you need to reproduce your schema in another environment or roll back a change. Migrations are version-controlled truth
  • Hardcoding the Supabase URL or keys — use environment variables. This is not optional. Hardcoded values break across environments and risk committing secrets to Git
  • Over-designing the schema upfront — start with 3-5 tables that cover your core data model. Add complexity as you learn what your users actually need, not what you imagine they might need
  • Not testing RLS policies — use the SQL Editor to run queries as different user contexts before shipping. A policy that looks correct can have subtle holes that expose data