My App
Backend

Base de données

PostgreSQL piloté par Drizzle, un schéma en modules avec des identifiants ULID.

La base est PostgreSQL 18, pilotée par Drizzle dans le package packages/db. Le schéma est découpé en modules, un fichier par domaine, tout réexporté depuis un index unique.

Le schéma en modules

packages/db/src/schema/
  auth.ts           # tables Better Auth (identifiants texte)
  identity.ts       # profile
  content.ts        # post, media
  social.ts         # follow, block, mute, reaction
  safety.ts         # report, moderation_action, protection_event, audit_log
  notifications.ts  # notification
  index.ts          # réexporte tout

Le client Drizzle reçoit l'ensemble du schéma, ce qui rend toutes les tables disponibles pour les requêtes.

// packages/db/src/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";

export function createDb() {
  return drizzle(env.DATABASE_URL, { schema });
}

Identifiants ULID

Les tables métier utilisent des identifiants ULID, triables dans le temps et adaptés à la pagination par curseur. Les tables de Better Auth gardent leurs propres identifiants texte.

import { ulid } from "ulidx";
import { text } from "drizzle-orm/pg-core";

export const ulidPk = () =>
  text("id").primaryKey().$defaultFn(() => ulid());

Les index qui comptent

Pour un réseau social, seuls quelques index sont vraiment structurants. Ils sont posés dès le départ.

TableIndexUsage
post(author_id, created_at)Fil de profil, chemin le plus sollicité
follow(follower_id, followee_id) et (followee_id)Fil et listes, dans les deux sens
report(status, priority, created_at)File de modération, triée comme une queue
reaction(post_id)Compteurs
notification(user_id, created_at)Fil de notifications

Migrations

Le schéma se traduit en SQL via drizzle-kit.

bun run db:generate   # génère la migration à partir du schéma
bun run db:push       # applique sur la base
bun run db:studio     # interface d'exploration

Le garde-fou de la pagination

Le fil d'actualité est le chemin le plus sollicité. On pagine toujours par curseur (l'ULID de la dernière ligne), jamais avec OFFSET. Un OFFSET 10000 scanne dix mille lignes pour les jeter, ce qui s'effondre en quelques semaines d'usage. Les identifiants ULID triables fournissent ce curseur gratuitement.

On this page