Skip to main content
Back to Blog

Supabase as a Game Backend: A Practical Guide

By Adil BouchnitaApril 8, 202615 min readSupabasePostgreSQLGame BackendEdge FunctionsGame DevelopmentIndie Dev
Illustrated game backend concept showing Supabase powering multiple game genres with PostgreSQL, auth, and Edge Functions

Supabase is an open-source Firebase alternative built on PostgreSQL that provides auth, storage, Edge Functions, Row Level Security, and a REST API via PostgREST. It's not designed as a game backend, but with the right architecture it can serve as one.

The first instinct when you need a game backend is to reach for something that calls itself a game backend. That sounds logical, but it means inheriting an opinionated architecture before you've even defined your own.

I've shipped a game on Supabase. The specifics are under NDA, but the patterns are not. What I found is that Supabase handles auth, storage, server logic, and structured game data surprisingly well, as long as you architect around what it actually is: a Postgres-backed platform with a clean API layer. Not a game server. Not a networking stack. A persistence and logic layer that happens to be very good at the things most game backends need.

This guide is engine-agnostic. Whether you're working in Unity, Godot, Unreal, or something custom, the architecture applies. If your engine can make HTTP calls and parse JSON, you can use Supabase as your backend.

The Backend Landscape

Before talking about Supabase specifically, it helps to understand what else is out there and what tradeoffs each option brings.

PlayFab is full-featured and Microsoft-backed, but its proprietary entity model means you're building around PlayFab's data assumptions. Migration is painful. You're locked in the moment you commit.

Nakama is open-source and Go-based, with strong real-time capabilities. You can self-host or use Heroic Cloud (a managed offering), but either way you're managing more game-specific infrastructure than a fully managed BaaS gives you out of the box.

Firebase is the easiest starting point. Google auth, Firestore, Cloud Functions. But Firestore is NoSQL, and once your game data gets relational (and it will), you're fighting the data model instead of building features.

Custom servers give you maximum flexibility and maximum operational burden. You own every line of code, every deployment, every scaling decision. For a solo dev or small team, that's often more infrastructure than game development.

Supabase sits in an interesting middle ground. You get a real Postgres database, a REST API via PostgREST, Edge Functions for server logic, Storage for binary assets, built-in auth, and Row Level Security. Low vendor lock because everything underneath is open-source Postgres. If you outgrow the hosted service, you can self-host or migrate to raw Postgres and keep your schema intact.

PlatformPlayFabNakamaFirebaseSupabaseCustom
Vendor LockHighLowMediumLowNone
Getting StartedEasyMediumEasyEasyHard
Engine SDKsGoodFairGoodLimitedN/A
Data ModelProprietarySQL/LuaNoSQLSQL (Postgres)Your choice

I reached for Supabase because I wanted a real database with real SQL, without managing my own infrastructure. The fact that it came with auth, storage, and serverless functions out of the box meant I could focus on building the game instead of building the backend for the backend.

Drake meme: rejecting Firebase NoSQL, approving Supabase Postgres When someone suggests using a document database for relational game data.

Why a Relational Database Makes Sense for Games

Most game data is inherently relational. Player profiles reference inventories. Inventories reference item definitions. Tournaments reference both players and scores. Leaderboards join players, scores, and time windows. Progression systems track relationships between achievements, quests, and player state.

NoSQL databases handle each of these individually just fine. The pain starts when you need to query across them. "Show me the top 10 players in this tournament who own a specific item" is a single SQL query with two joins. In Firestore, it's multiple reads, client-side filtering, and a data model you'll restructure three times before it works.

Postgres handles relational game data naturally. Foreign keys enforce referential integrity (no orphaned inventory items pointing to deleted players). Indexes make leaderboard queries fast. Transactions keep concurrent score submissions consistent. And when you do need flexible, schemaless data (player settings, custom loadouts, event configurations), JSONB columns let you store structured JSON inside your relational tables without sacrificing the ability to query it.

The relational model isn't just a preference. For most game backends, it's the right tool.

Structuring Game Data in Postgres

Game concepts map cleanly to database tables. Here's a minimal schema that covers the core of most indie games: player identity, inventory, and live events.

-- Player profiles linked to auth
create table profiles (
  id uuid references auth.users primary key,
  display_name text unique not null,
  level int default 1,
  settings jsonb default '{}'
);

-- Game economy
create table inventories (
  id uuid primary key default gen_random_uuid(),
  player_id uuid references profiles(id),
  item_type text not null,
  quantity int default 1,
  metadata jsonb default '{}'
);

-- Live events
create table tournaments (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  config jsonb default '{}'
);

A few things worth noting about this structure.

Foreign keys do the heavy lifting. The profiles.id column references auth.users, so every profile is automatically tied to an authenticated user. The inventories.player_id references profiles(id), so you can never have inventory items floating around without an owner. The database enforces these relationships. Your application code doesn't have to.

JSONB handles the flexible parts. Not everything in a game fits neatly into typed columns. Player settings (control preferences, UI layout, notification toggles) vary per player and change shape over time. Item metadata (enchantments, custom colors, upgrade paths) differs per item type. Tournament config (scoring rules, entry fees, prize pools) changes per event. JSONB columns let you store this kind of semi-structured data inside your relational schema, queryable and indexable, without needing a separate document store.

Timestamps with time zones matter. Tournaments that start at "8pm" need to mean the same thing regardless of where the server or player is located. Using timestamptz and storing everything in UTC saves you from an entire category of bugs that only surface when your players span multiple time zones.

If you're building something with AI-powered NPCs or semantic search (item descriptions, quest logs), Postgres also supports vector columns via the pgvector extension. That's a topic for another post, but it's worth knowing the database can grow with you.

What Supabase Brings to the Table

Supabase wraps Postgres with a set of services that cover most of what a game backend needs. Here's how each one maps to game development.

Auth

Supabase Auth handles email/password registration, OAuth providers (Google, Discord, Apple, Steam requires a custom Edge Function to bridge its OpenID 2.0 flow to Supabase's OAuth 2.0-based auth), and JWT issuance. You get a complete authentication system without building or maintaining an auth server.

Every authenticated request carries a JWT. Your game client stores the token after login and includes it in every subsequent request. The token contains the user's ID, which Row Level Security policies use to determine what data that user can access. No middleware. No session management on your end.

For most indie games, email/password plus one or two OAuth providers covers the entire auth surface. That's maybe twenty minutes of setup in the Supabase dashboard.

Storage

Supabase Storage is S3-compatible object storage. For games, this means avatars, user-generated content (levels, skins, replays), and downloadable asset bundles.

Upload is a PUT request with the JWT for authorization. Download URLs are predictable if the bucket is public, or signed if private. The client can construct download paths without extra API calls if you follow a consistent naming convention.

One caveat: at scale, Supabase Storage pricing can add up for high-bandwidth use cases like large asset bundles downloaded by thousands of players. If you're serving multi-megabyte files to a large player base, consider offloading to a dedicated S3 bucket or CDN. For most indie-scale projects, Supabase Storage is fine.

Realtime

Supabase Realtime broadcasts Postgres changes over WebSockets. When a row in your database changes, connected clients can receive the update instantly. This is useful for lobby presence (who's online), live leaderboards (scores updating in real time), and notification systems (tournament starting, friend request received).

But let's be honest about the limits. Supabase Realtime caps at around 10,000 concurrent connections on Pro and Team plans (lower on Free, higher on Enterprise with custom quotas), and performance degrades well before those numbers depending on message volume. This is not a replacement for real-time multiplayer networking. If you need frame-rate state sync (player positions updating 30+ times per second), you need a dedicated networking solution: Photon, Netcode for GameObjects, or custom UDP servers.

Realtime is a notification channel, not a game networking layer. Use it for infrequent, event-driven updates. Keep your subscription count low and your message payloads small.

Edge Functions

Edge Functions are Supabase's serverless compute layer (Deno-based, deployed globally on the edge). Because they run close to your players geographically, request latency is naturally lower than routing everything through a single central server. For games, this is where server-authoritative logic lives. Score validation, tournament lifecycle management, reward distribution, integration with external APIs (payment providers, analytics, third-party services).

The key principle: Edge Functions should orchestrate, not compute. A function that validates a score submission, writes it to the database, and returns a result is a good use case. A function that runs heavy game simulation logic is not. Keep them single-purpose, lightweight, and fast.

Cold starts are worth planning for. Supabase has improved them on paper, but real-world results vary and some developers still report longer cold starts than advertised. On top of that, execution time itself can add up if you're doing heavy work inside a function: chaining multiple database queries, calling external APIs, or processing data. Design your game flow around both. Use polished transition animations to mask latency, pre-warm critical functions on login, and keep the functions themselves lightweight so execution stays fast.

Look at me, I'm the middleware now Row Level Security replacing your entire auth middleware layer.

Row Level Security

Row Level Security (RLS) is one of the most underappreciated features for game backends. It moves access control from your application code into the database itself.

With RLS enabled, you write policies like "players can only read and write their own profile" or "players can read tournament data but only admins can modify it." These policies are enforced at the database level. Even if a malicious client crafts a custom request, the database will reject it. No middleware layer. No authorization checks scattered across your API endpoints.

For a solo dev or small team, this is a significant reduction in surface area for security bugs. The database handles it. You define the rules once. They apply everywhere.

JSONB and Vectors

I mentioned JSONB columns earlier in the schema section. At the Supabase level, the important thing is that PostgREST (the REST API layer) can query into JSONB fields. You can filter, sort, and select nested JSON properties via query parameters. This means your flexible data (player settings, item metadata, event config) is still queryable through the API without custom server logic.

Vector columns via pgvector enable similarity search directly in your database. If you're building AI-powered features (NPC dialogue, semantic item search, content recommendations), the vector support means you don't need a separate vector database. It all lives in the same Postgres instance.

Making It Work in Production

Knowing what Supabase offers is one thing. Making it perform well in a production game is another. Here are the practical lessons that made the difference.

  1. Filter queries aggressively. PostgREST supports column selection and row filtering via query parameters. Every request should specify exactly which columns it needs. A leaderboard query that only needs rank, display name, and score should not pull back the entire player profile with settings, inventory metadata, and timestamps. This matters especially with JSONB columns, where a naive select pulls back large nested objects the client doesn't need.

  2. Index tables properly. Leaderboard queries on a scores table with 100,000 rows are fast with the right index. Without one, they're slow enough to notice. Add indexes on columns you filter and sort by. For leaderboards, that's typically the score column plus a tournament ID. For inventories, it's the player ID. This is standard database practice, but it's easy to skip during prototyping and painful to debug later.

  3. Realtime is a tool, not a default. Don't subscribe to Realtime channels for everything. Subscribe to the leaderboard channel when the player opens the leaderboard screen. Unsubscribe when they leave. Subscribe to lobby presence when they're in the lobby. Don't maintain persistent subscriptions to tables the player isn't actively viewing. Stay well below the concurrent connection ceiling and keep message volume low.

  4. Reduce client chattiness. This is the single most important architectural decision for a Supabase game backend. Batch reads on login: pull the player's profile, inventory, and active tournaments in a single burst, not across three separate screens. Submit scores once at the end of a round, not incrementally during gameplay. Sync user-generated content metadata in bulk when the player opens the browser, not on every scroll. Fewer round trips means lower latency, fewer rate limit concerns, and fewer failure points.

  5. Keep Edge Functions lightweight. Each function should do one thing. Validate a score. Create a tournament entry. Process a webhook. Don't chain multiple database operations inside a single function call. If you need multi-step pipelines (create a job, wait for external processing, update on callback), use a job table pattern: write the job row, return immediately, and handle the next step via webhook or scheduled check.

  6. Plan for Edge Function latency. Cold starts can still surprise you despite improvements, and execution time adds up if functions do too much. Pre-warm your most-used functions during the loading screen. Use transition animations between screens to mask round-trip time. Keep function bodies lean so execution stays fast.

  7. Consider S3 for storage at scale. Supabase Storage works great for development and moderate traffic. If you're serving large asset bundles to thousands of concurrent players, a dedicated S3 bucket with CloudFront or another CDN in front of it will be cheaper and faster.

Not sure if game client or DDoS attack What your Supabase dashboard looks like when your game client polls every frame.

No SDK, No Problem

Supabase has official SDKs for JavaScript, Python, Dart, and Swift. It does not have an official SDK for C#, GDScript, or C++. For game engines, that might sound like a dealbreaker. It's not.

Supabase's API is just HTTP. Every operation, from auth to database queries to storage uploads, is a REST call. If your engine can make HTTP requests and parse JSON responses, you can talk to Supabase directly.

The auth pattern is straightforward. Every request includes two headers: the apikey header with the project's public anon key, and an Authorization header with a Bearer <jwt> after login. Registration hits /auth/v1/signup with an email and password in the body. Login hits /auth/v1/token?grant_type=password. Both return a JWT that the client stores in memory and attaches to every subsequent request.

Database queries go through PostgREST endpoints. Reading a player's profile is a GET request to /rest/v1/profiles?id=eq.{user_id}&select=display_name,level. Inserting a score is a POST to /rest/v1/scores with the data in the body. Filtering, sorting, pagination, and column selection are all query parameters.

Can't have SDK issues if you don't use an SDK No C# SDK? No problem. Raw HTTP works across every engine.

I've done this in production. The HTTP-based approach is clean, debuggable, and completely engine-agnostic. No SDK dependency means no version conflicts, no waiting for SDK updates when Supabase ships new features, and no black-box abstractions hiding the actual API calls. You see every request, every response, every error. For a solo dev debugging at 2am, that transparency is worth more than any SDK convenience.

When Supabase Fits (and When It Doesn't)

After building with Supabase in production, here's a clear-eyed assessment.

Use Supabase when

  • Your game client isn't chatty. Backend interactions are infrequent, batched, and event-driven (login, score submission, content browsing), not continuous.
  • You want to build fast without managing infrastructure. Auth, database, storage, and serverless functions in one platform, no DevOps required.
  • You don't want vendor lock-in. Everything runs on Postgres. You can self-host Supabase or migrate to raw Postgres and keep your schema, your queries, and your RLS policies.
  • Your backend is mostly CRUD with server-side validation. Profiles, inventories, leaderboards, tournaments, user-generated content. The bread and butter of game persistence.

Don't use Supabase when

  • Your game clients are chatty. If players are sending and receiving messages to each other multiple times per second (real-time chat, cooperative gameplay, live trading), you need a dedicated messaging or networking layer.
  • You need frame-rate state sync. Real-time multiplayer where player positions, physics, or game state update 30+ times per second is not what Supabase Realtime is built for. Use Photon, Netcode for GameObjects, or custom UDP servers.
  • You're planning for massive concurrent scale. Hundreds of thousands of concurrent users hitting your backend simultaneously is beyond what Supabase's hosted plans are designed for. At that scale, you need custom infrastructure regardless of which platform you started with.

The honest take: Supabase handles more than you'd expect from a platform that doesn't market itself as a game backend. Auth, storage, structured data, server logic, access control. That covers a lot of ground. But it only works because the architecture respects what Supabase is good at and doesn't ask it to be a real-time networking stack. The moment you need sub-100ms server authority over game state at high frequency, you need a different tool for that specific job.

Game backend pointing at CRUD app: they're the same Most game backends are just CRUD apps with extra steps.

If you're evaluating backend options for a game project, the question isn't "can Supabase do everything?" It's "can I architect my game so that the real-time parts and the persistent parts live in separate layers?" If yes, Supabase is a strong candidate for the persistent side.

Take a look at my projects to see how different architectures play out across different types of applications. If you're also making decisions about how to deliver a real-time interactive experience in the browser, my comparison of Unity WebGL, Three.js, and Pixel Streaming covers the client-side half of the equation. And if you're weighing options for your own game backend or need help with the architecture, hire me.

Adil Bouchnita
Adil Bouchnita

Senior Unity Engineer & Technical Artist with 13+ years building real-time 3D experiences. Shaders, multiplayer systems, environment art, and WebGL optimization.