Supabase, un backend open source construit sur PostgreSQL

Supabase associe PostgreSQL, authentification, stockage et fonctions dans un seul projet. Tarifs, sécurité au niveau des lignes et erreurs qui coûtent cher.

L'équipe CodeWorlds21 min de lecture ·

Pratique Next.js dans des leçons interactives avec un éditeur de code.

Crée un compte gratuit→
Dans cet article19

Supabase est un ensemble de services construits autour d'un PostgreSQL ordinaire : une base de données, l'authentification des utilisateurs, le stockage de fichiers, des fonctions à la demande et des abonnements aux changements en temps réel. Le code de la plateforme est open source sous licence Apache 2.0, la bibliothèque cliente sous licence MIT, et le dépôt affiche plus de cent sept mille étoiles.

La principale différence avec la concurrence

Tout le reste découle d'une seule décision : un vrai PostgreSQL se trouve dessous, plutôt qu'un moteur sur mesure avec son propre langage de requête.

Son importance se voit dès qu'un projet cesse d'être simple. Tu disposes des jointures, des vues, des déclencheurs, des fonctions dans la base, des index partiels et de tout l'écosystème d'extensions, y compris la recherche plein texte et le travail avec des vecteurs. Les solutions construites sur leur propre modèle de données commencent à ce moment-là à avoir besoin de contournements, alors qu'ici tu écris simplement une requête.

La deuxième conséquence concerne la sortie. Puisqu'il s'agit d'un PostgreSQL ordinaire, tu exportes les données avec un outil standard et tu les charges ailleurs. Ce n'est pas un avantage marketing mais un avantage réel, car changer de fournisseur revient à déplacer la base de données et à remplacer une chaîne de connexion, plutôt qu'à réécrire la couche d'accès aux données.

La troisième chose, c'est le fonctionnement de l'API. L'interface qui expose les tables en HTTP est générée automatiquement à partir du schéma de la base, donc ajouter une colonne apparaît immédiatement dans l'API sans écrire une ligne de code. Pratique pendant le prototypage et risqué plus tard, ce qu'on verra dans un instant.

Quand c'est un bon choix

Ça vaut le coup de nommer ces scénarios clairement, car cette plateforme est souvent choisie par réflexe, et tous les projets n'en tirent pas profit.

Elle fonctionne le mieux pour un produit construit par une petite équipe, là où arriver vite à une application qui marche compte vraiment. Authentification, base de données, fichiers et fonctions au même endroit signifient que la première semaine est consacrée au produit plutôt qu'à assembler une infrastructure pièce par pièce. C'est une vraie économie, et la principale raison du succès de cette plateforme.

Le deuxième bon scénario, c'est un projet dont les données sont relationnelles et où une requête avec plusieurs jointures, ou un rapport, apparaîtra inévitablement. Choisir une base de données documentaire dans ce cas signifie que cette fonctionnalité arrivera plus tard, et moins bien.

Elle s'en sort moins bien dans deux situations. La première, c'est une application avec une logique métier serveur conséquente, où tu as de toute façon besoin de ta propre couche de services. La partie de la plateforme consacrée à l'accès depuis le navigateur devient alors inutile, et ce qui reste, c'est du PostgreSQL hébergé, que tu peux obtenir moins cher ailleurs, chez Neon par exemple.

La seconde, c'est l'obligation de garder les données sur ta propre infrastructure. Une variante auto-hébergée existe et est open source, mais elle comprend une bonne douzaine de services, donc la maintenir est un vrai travail, pas juste démarrer un conteneur. Chiffre ça honnêtement avant de décider. Sur un petit projet, PocketBase offre la même chose sous la forme d'un seul exécutable qui embarque une base SQLite, l'authentification, les fichiers et une API. Le prix de cette simplicité, c'est un seul processus sans réplication et un statut antérieur à la version 1.0, où la compatibilité ascendante n'est pas garantie et où une mise à jour demande parfois des étapes manuelles.

La sécurité au niveau des lignes, la partie critique

C'est la section la plus importante de ce texte, car elle couvre le seul endroit où une erreur coûte une fuite de données.

Puisque le client se connecte à la base de données directement depuis un navigateur, la seule barrière entre un utilisateur et les données de quelqu'un d'autre, ce sont les règles d'accès définies dans la base elle-même. Il n'y a pas de couche serveur applicatif qui vérifierait les permissions au passage. Une table sans règles activées est visible par n'importe qui connaissant l'adresse du projet et la clé publique, et cette clé arrive dans le navigateur par définition.

Le symptôme est traître, car il n'y en a pas. L'application fonctionne correctement, les tests passent, et les données sont accessibles à tout le monde. La découverte se fait généralement quand un inconnu ouvre l'onglet réseau de ses outils de développement.

La règle pratique est donc la suivante : active les règles d'accès sur chaque table immédiatement après sa création, avant d'écrire la moindre ligne de code client. Une table verrouillée par défaut se traduit par une liste vide, ce que tu remarques tout de suite, contrairement à une table ouverte.

Le deuxième piège concerne la clé de service. À côté de la clé publique, il en existe une seconde qui contourne toutes les règles d'accès. Elle appartient exclusivement au code serveur, et son apparition dans une variable accessible depuis le navigateur équivaut à livrer toute la base de données. Dans Next.js, la frontière, c'est le préfixe du nom de la variable d'environnement, et ça vaut le coup de le vérifier deux fois.

La troisième chose, c'est le coût des règles. Une règle est une condition ajoutée à chaque requête, donc une règle qui s'appuie sur une sous-requête vers une autre table s'exécute pour chaque ligne. À mille enregistrements, personne ne le remarque ; à un million, la requête ne répond plus. Si une liste a soudainement ralenti après l'ajout de règles, c'est le premier endroit à vérifier.

Qu'est-ce que Supabase et pourquoi est-il si populaire ?

Supabase est un Backend-as-a-Service (BaaS) open source, souvent appelé « le Firebase open source ». Mais contrairement à Firebase, qui utilise du NoSQL (Firestore), Supabase est construit sur PostgreSQL, la base de données relationnelle open source la plus puissante.

Supabase offre tout ce qu'il faut pour construire une application moderne :

  • Base de données PostgreSQL avec un query builder puissant
  • Authentification avec connexions sociales et magic links
  • Storage pour les fichiers et les images
  • Abonnements en temps réel (Real-time)
  • Edge Functions (Deno)
  • Vector embeddings pour l'IA

Pourquoi choisir Supabase ?

Open source

Tout le code de Supabase est disponible sur GitHub. Tu peux l'auto-héberger, le modifier, et avoir la certitude de ne pas être enfermé chez un seul fournisseur.

PostgreSQL sous le capot

Tu as toute la puissance de PostgreSQL :

  • Transactions ACID
  • Clés étrangères et contraintes
  • Recherche plein texte
  • Support JSON/JSONB
  • Extensions (PostGIS, pg_vector, etc.)
  • Row Level Security

Expérience développeur

Un excellent SDK, une documentation d'API générée automatiquement, un dashboard pour gérer les données, et une intégration facile avec les frameworks populaires.

Installation et configuration

Création d'un projet

  1. Crée un compte sur supabase.com
  2. Crée un nouveau projet
  3. Enregistre l'URL et les clés API

Installation du SDK

Bash
# JavaScript/TypeScript
npm install @supabase/supabase-js

# Hooks spécifiques à React
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs

Configuration du client

TSlib/supabase.ts
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

// Types générés par la CLI Supabase
import { Database } from '@/types/database.types'

export const supabase = createClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Client côté serveur (Next.js App Router)
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export const createServerClient = () => {
  return createServerComponentClient<Database>({ cookies })
}

Variables d'environnement

.env.local
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...  # Uniquement côté serveur !

Base de données PostgreSQL

Création des tables

Dans le Dashboard Supabase ou en SQL :

SQL
-- Table des utilisateurs (étend auth.users)
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  full_name TEXT,
  avatar_url TEXT,
  bio TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Table des articles
CREATE TABLE public.posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  author_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  slug TEXT UNIQUE NOT NULL,
  published BOOLEAN DEFAULT FALSE,
  published_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index pour les performances
CREATE INDEX posts_author_id_idx ON public.posts(author_id);
CREATE INDEX posts_published_idx ON public.posts(published) WHERE published = true;
CREATE INDEX posts_slug_idx ON public.posts(slug);

Opérations CRUD

TypeScript
// SELECT - récupération des données
const { data: posts, error } = await supabase
  .from('posts')
  .select(`
    id,
    title,
    slug,
    content,
    published_at,
    author:profiles(id, username, avatar_url)
  `)
  .eq('published', true)
  .order('published_at', { ascending: false })
  .limit(10)

// SELECT avec filtrage
const { data } = await supabase
  .from('posts')
  .select('*')
  .or('title.ilike.%react%,content.ilike.%react%')
  .gte('published_at', '2024-01-01')
  .lte('published_at', '2024-12-31')

// INSERT
const { data: newPost, error } = await supabase
  .from('posts')
  .insert({
    author_id: userId,
    title: 'Mon premier article',
    slug: 'mon-premier-article',
    content: 'Bonjour le monde !',
  })
  .select()
  .single()

// UPDATE
const { data, error } = await supabase
  .from('posts')
  .update({
    title: 'Titre mis à jour',
    updated_at: new Date().toISOString(),
  })
  .eq('id', postId)
  .select()
  .single()

// DELETE
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId)

// UPSERT (création ou mise à jour)
const { data, error } = await supabase
  .from('profiles')
  .upsert({
    id: userId,
    username: 'john_doe',
    full_name: 'John Doe',
  })
  .select()
  .single()

Requêtes avancées

TypeScript
// Pagination
const pageSize = 10
const page = 1

const { data, count } = await supabase
  .from('posts')
  .select('*', { count: 'exact' })
  .range((page - 1) * pageSize, page * pageSize - 1)

// Recherche plein texte
const { data } = await supabase
  .from('posts')
  .select('*')
  .textSearch('title', 'react hooks', {
    type: 'websearch',
    config: 'english'
  })

// Agrégations via RPC
// Crée d'abord une fonction en SQL :
// CREATE FUNCTION get_post_stats() RETURNS TABLE (total bigint, published bigint)
// AS $$ SELECT COUNT(*), COUNT(*) FILTER (WHERE published) FROM posts $$
// LANGUAGE sql;

const { data } = await supabase.rpc('get_post_stats')

Authentification

Email et mot de passe

TypeScript
// Inscription
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'securepassword123',
  options: {
    data: {
      full_name: 'John Doe',
    },
    emailRedirectTo: 'https://myapp.com/auth/callback',
  },
})

// Connexion
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'securepassword123',
})

// Déconnexion
await supabase.auth.signOut()

// Réinitialisation du mot de passe
await supabase.auth.resetPasswordForEmail('user@example.com', {
  redirectTo: 'https://myapp.com/reset-password',
})

OAuth (connexions sociales)

TypeScript
// GitHub
await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://myapp.com/auth/callback',
    scopes: 'read:user user:email',
  },
})

// Google
await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://myapp.com/auth/callback',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent',
    },
  },
})

// Fournisseurs pris en charge :
// google, github, gitlab, bitbucket, azure, discord,
// facebook, twitter, apple, spotify, slack, twitch, notion

Magic Link (sans mot de passe)

TypeScript
const { error } = await supabase.auth.signInWithOtp({
  email: 'user@example.com',
  options: {
    emailRedirectTo: 'https://myapp.com/auth/callback',
  },
})

Session utilisateur

TypeScript
// Récupère la session actuelle
const { data: { session } } = await supabase.auth.getSession()

// Récupère l'utilisateur
const { data: { user } } = await supabase.auth.getUser()

// Écoute les changements d'authentification
supabase.auth.onAuthStateChange((event, session) => {
  console.log('Événement auth :', event)
  console.log('Session :', session)

  if (event === 'SIGNED_IN') {
    // Utilisateur connecté
  } else if (event === 'SIGNED_OUT') {
    // Utilisateur déconnecté
  } else if (event === 'TOKEN_REFRESHED') {
    // Le token a été rafraîchi
  }
})

Intégration avec Next.js App Router

TSapp/auth/callback/route.ts
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = createRouteHandlerClient({ cookies })
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/', request.url))
}
TSmiddleware.ts
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  const { data: { session } } = await supabase.auth.getSession()

  // Protège les routes qui nécessitent une authentification
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', req.url))
  }

  return res
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
}

Row Level Security (RLS)

RLS est une fonctionnalité de sécurité clé de Supabase. Elle permet de définir des politiques d'accès au niveau de la ligne.

SQL
-- Active RLS sur la table
ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;

-- Politique : tout le monde peut lire les articles publiés
CREATE POLICY "Public posts are viewable by everyone"
ON public.posts FOR SELECT
USING (published = true);

-- Politique : les utilisateurs peuvent lire leurs propres articles
CREATE POLICY "Users can view own posts"
ON public.posts FOR SELECT
USING (auth.uid() = author_id);

-- Politique : les utilisateurs peuvent créer des articles
CREATE POLICY "Users can create posts"
ON public.posts FOR INSERT
WITH CHECK (auth.uid() = author_id);

-- Politique : les utilisateurs peuvent modifier leurs propres articles
CREATE POLICY "Users can update own posts"
ON public.posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);

-- Politique : les utilisateurs peuvent supprimer leurs propres articles
CREATE POLICY "Users can delete own posts"
ON public.posts FOR DELETE
USING (auth.uid() = author_id);

-- Politique avec des rôles (par ex. admin)
CREATE POLICY "Admins can do everything"
ON public.posts FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM public.profiles
    WHERE profiles.id = auth.uid()
    AND profiles.role = 'admin'
  )
);

Storage

Envoi de fichiers

TypeScript
// Envoi depuis le navigateur
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const fileName = `${userId}/${Date.now()}.${fileExt}`

const { data, error } = await supabase.storage
  .from('avatars')
  .upload(fileName, file, {
    cacheControl: '3600',
    upsert: true,
  })

// Récupère l'URL publique
const { data: { publicUrl } } = supabase.storage
  .from('avatars')
  .getPublicUrl(fileName)

Téléchargement et suppression

TypeScript
// Téléchargement
const { data, error } = await supabase.storage
  .from('documents')
  .download('folder/file.pdf')

// Liste des fichiers
const { data: files } = await supabase.storage
  .from('documents')
  .list('folder', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'created_at', order: 'desc' },
  })

// Suppression
const { error } = await supabase.storage
  .from('avatars')
  .remove(['avatar1.png', 'avatar2.png'])

Politiques de stockage

SQL
-- Politique : les utilisateurs peuvent envoyer des fichiers dans leur propre dossier
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' AND
  auth.uid()::text = (storage.foldername(name))[1]
);

-- Politique : accès en lecture public
CREATE POLICY "Avatars are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

Abonnements en temps réel

TypeScript
// Écoute les changements de la table
const channel = supabase
  .channel('posts-changes')
  .on(
    'postgres_changes',
    {
      event: '*', // INSERT, UPDATE, DELETE ou *
      schema: 'public',
      table: 'posts',
      filter: 'published=eq.true', // Filtre optionnel
    },
    (payload) => {
      console.log('Changement reçu !', payload)

      if (payload.eventType === 'INSERT') {
        // Nouvel article
        setPosts(prev => [payload.new, ...prev])
      } else if (payload.eventType === 'UPDATE') {
        // Article mis à jour
        setPosts(prev =>
          prev.map(p => p.id === payload.new.id ? payload.new : p)
        )
      } else if (payload.eventType === 'DELETE') {
        // Article supprimé
        setPosts(prev => prev.filter(p => p.id !== payload.old.id))
      }
    }
  )
  .subscribe()

// Nettoyage
return () => {
  supabase.removeChannel(channel)
}

Broadcast (événements personnalisés)

TypeScript
// Envoi
const channel = supabase.channel('room:123')
channel.send({
  type: 'broadcast',
  event: 'cursor-position',
  payload: { x: 100, y: 200 },
})

// Réception
channel.on('broadcast', { event: 'cursor-position' }, (payload) => {
  console.log('Position du curseur :', payload.payload)
})

Présence (statut en ligne)

TypeScript
const channel = supabase.channel('room:123')

// Suit la présence
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState()
  console.log('Utilisateurs en ligne :', Object.keys(state).length)
})

channel.on('presence', { event: 'join' }, ({ key, newPresences }) => {
  console.log('Utilisateur a rejoint :', key)
})

channel.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
  console.log('Utilisateur a quitté :', key)
})

// Suit l'utilisateur actuel
channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({
      user_id: userId,
      online_at: new Date().toISOString(),
    })
  }
})

Edge Functions

Les Edge Functions sont des fonctions serverless écrites en TypeScript et exécutées sur Deno :

TSsupabase/functions/send-email/index.ts
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

serve(async (req) => {
  const { to, subject, body } = await req.json()

  // Envoie l'email avec, par exemple, Resend
  const response = await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'noreply@myapp.com',
      to,
      subject,
      html: body,
    }),
  })

  const data = await response.json()

  return new Response(
    JSON.stringify(data),
    { headers: { 'Content-Type': 'application/json' } }
  )
})

Appel depuis le client :

TypeScript
const { data, error } = await supabase.functions.invoke('send-email', {
  body: {
    to: 'user@example.com',
    subject: 'Bienvenue !',
    body: '<h1>Bienvenue dans notre application</h1>',
  },
})

Pour un message unique, cet appel suffit. Dès que des séquences de bienvenue et des rappels de fin d'essai apparaissent, il est plus simple d'envoyer l'événement lui-même depuis la fonction vers un outil comme Loops, qui garde le contenu et les conditions d'envoi de son côté. Il facture cependant au contact plutôt qu'au message, donc une liste d'adresses mortes coûte le même prix qu'une liste active.

Génération des types TypeScript

Bash
# Installe la CLI Supabase
npm install -g supabase

# Connexion
supabase login

# Génère les types
supabase gen types typescript --project-id your-project-id > types/database.types.ts

Utilisation des types générés :

TypeScript
import { Database } from '@/types/database.types'

type Post = Database['public']['Tables']['posts']['Row']
type NewPost = Database['public']['Tables']['posts']['Insert']
type UpdatePost = Database['public']['Tables']['posts']['Update']

// Tu as maintenant un typage complet
const { data } = await supabase
  .from('posts')
  .select('*')
  .single()
// data est de type Post | null

Comparaison Supabase vs Firebase

CaractéristiqueSupabaseFirebase
Base de donnéesPostgreSQL (relationnelle)Firestore (NoSQL)
Open SourceOuiNon
Auto-hébergementOuiNon
SQLSupport completAucun
RelationsClés étrangères, JOINsAucune
Temps réelChangements PostgreSQLSnapshots Firestore
AuthCapacités similairesCapacités similaires
StorageCapacités similairesCapacités similaires
Edge FunctionsDenoNode.js
TarificationPlus transparentePay-as-you-go

Tarifs (2025)

Free

  • 500 MB de base de données
  • 1 GB de stockage
  • 2 GB de bande passante
  • 50 000 MAU (auth)
  • 500 000 invocations d'Edge Functions

Pro ($25/mois)

  • 8 GB de base de données
  • 100 GB de stockage
  • 250 GB de bande passante
  • 100 000 MAU
  • 2M invocations d'Edge Functions
  • Sauvegardes quotidiennes

Team ($599/mois)

  • Tout ce qui est inclus dans Pro
  • Conformité SOC2
  • SSO/SAML
  • Support prioritaire
  • PITR sur 14 jours

Enterprise (sur mesure)

  • Infrastructure dédiée
  • Contrats personnalisés
  • Garanties de SLA

Bonnes pratiques

1. Utilise toujours RLS

SQL
-- Ne laisse jamais de tables sans RLS en production !
ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;

2. Utilise les types TypeScript

Bash
# Régénère les types après chaque changement de schéma
supabase gen types typescript --project-id xxx > types/database.types.ts

3. Optimise les requêtes

TypeScript
// Sélectionne uniquement les colonnes dont tu as besoin
const { data } = await supabase
  .from('posts')
  .select('id, title, slug') // Pas select('*')
  .limit(10)

4. Utilise des index

SQL
-- Ajoute des index pour les colonnes souvent filtrées
CREATE INDEX posts_published_idx ON posts(published_at)
WHERE published = true;

FAQ

Supabase est-il gratuit ?

Oui, le plan Free permet de construire et de tester des applications. Pour la production, le plan Pro est recommandé.

Puis-je auto-héberger Supabase ?

Oui, tous les composants sont open source. La documentation inclut des instructions pour Docker et Kubernetes.

Comment migrer depuis Firebase ?

Supabase propose des outils de migration. Le principal changement, c'est le passage d'un modèle de données NoSQL à un modèle SQL.

Supabase est-il scalable ?

Oui, PostgreSQL est reconnu pour sa bonne scalabilité. Supabase propose aussi des read replicas et le connection pooling.

Tarifs et le piège du plan gratuit

PlanPrix mensuelBase de donnéesFichiersSortie (egress)Utilisateurs actifs
Free0 USD500 MB1 GB5 GB50k
Pro25 USD8 GB par projet100 GB250 GB100k
Team599 USD8 GB par projet100 GB250 GB100k
Enterprisedevis personnalisénégociénégociénégociénégocié

Le plan payant inclut dix dollars de crédit de calcul, qui couvrent la plus petite instance, tout ce qui est plus gros étant facturé en plus. Le nombre de requêtes à l'API est illimité sur tous les plans.

Le crédit s'applique cependant par organisation, alors que le calcul se facture par projet, et c'est la surprise la plus courante sur cette plateforme. Une organisation avec un seul projet sur la plus petite instance paie vingt-cinq dollars, puisque le crédit couvre la totalité. Un deuxième projet sur la même instance fait monter la facture à trente-cinq dollars, bien que le plan n'ait pas changé. Avec plusieurs environnements, où un projet séparé sert de copie de préproduction, compte cette ligne une fois par environnement plutôt qu'une seule fois au total.

La chose la plus importante à propos du plan gratuit ne concerne cependant pas les limites, mais la mise en pause. Les projets gratuits sont mis en pause après une semaine d'inactivité, et la limite de projets actifs est de deux. Pour l'apprentissage et les expérimentations, ça n'a guère d'importance, puisqu'un projet reprend manuellement. Pour tout ce qui doit être accessible à d'autres personnes, c'est disqualifiant : une application montrée à un client après une coupure de deux semaines ne répondra pas.

Quand tu estimes les coûts, surveille la sortie (egress), car c'est la ligne la plus souvent sous-estimée. Servir des images directement depuis le stockage de fichiers consomme cette allocation plus vite que tout le reste, en particulier quand les fichiers ne passent pas par un réseau de diffusion de contenu. Si ton application distribue des photos, chiffre ça séparément avant de choisir un plan.

Erreurs courantes

La première, c'est une table sans règles d'accès, déjà évoquée plus haut. C'est la seule erreur de cette liste qui se termine par une fuite de données plutôt que par une panne.

La deuxième, c'est de garder des connexions sans pooling. PostgreSQL impose une limite stricte sur les connexions simultanées, et un environnement d'exécution qui invoque des fonctions à la demande en ouvre une nouvelle à chaque appel. Supabase expose une adresse groupée séparée, et c'est celle que le code sans état doit utiliser. Les migrations de schéma et les tâches d'administration longues ont cependant besoin d'une connexion directe, car le mode transactionnel du pool ne prend pas tout en charge.

La troisième, c'est de compter sur l'API générée automatiquement pour une logique complexe. Une interface qui expose des tables fonctionne très bien pour lire une liste et écrire un enregistrement, et cesse de suffire dès qu'une opération doit modifier trois tables de façon atomique. C'est le rôle des fonctions en base invoquées comme des procédures, pas d'une chaîne de requêtes séparées depuis un navigateur, car cette chaîne ne porte aucune garantie de cohérence.

La quatrième concerne les abonnements aux changements en temps réel. C'est tentant de les brancher partout, mais chaque abonnement est une connexion ouverte et un flux d'événements à gérer côté client. Pour une liste mise à jour une fois par heure, un simple fetch est moins cher et plus simple ; garde le mécanisme temps réel pour ce qui change réellement sous les yeux d'un utilisateur.

La cinquième, c'est d'oublier les migrations. Cliquer dans le dashboard est rapide et pratique, et six mois plus tard, personne ne sait pourquoi une colonne a cette forme ni comment recréer la base de données depuis zéro. Garder les changements de schéma dans des fichiers de migration versionnés dans le dépôt, c'est la même discipline que garder le code sous contrôle de version.

La sixième, visible seulement au premier trafic sérieux, c'est l'absence d'index pour les requêtes que tu exécutes réellement. La base de données suppose un index sur la clé primaire et sur les clés étrangères, mais pas sur la colonne qui sert à filtrer une liste, ni sur la combinaison de colonnes utilisée pour le tri. À quelques centaines de lignes, la différence est invisible, puisque la base lit tout de toute façon ; à quelques centaines de milliers, la même page cesse de s'ouvrir. Regarde de temps en temps les requêtes les plus lentes dans le dashboard et vérifie si l'une d'elles parcourt une table entière.

Le code source se trouve dans le dépôt Supabase, et les plans actuels sur la page des tarifs.

À lire aussi