Documentation

Ajouter une nouvelle table

Tutoriel pas à pas — créer un schéma Drizzle, l'exporter, générer une migration, l'appliquer et écrire des requêtes dans Motoko Base.

Ouvrir dansChatGPT (s’ouvre dans un nouvel onglet)Claude (s’ouvre dans un nouvel onglet)Cursor (s’ouvre dans un nouvel onglet)Tutoriel pas à pas — créer un schéma Drizzle, l'exporter, générer une migration, l'appliquer et écrire des requêtes dans Motoko Base.

Ce walkthrough ajoute une table notes — une ressource simple appartenant à l'utilisateur, similaire à Link Manager. Adaptez les mêmes étapes pour toute nouvelle feature.

Objectif : chaque utilisateur peut stocker des notes avec un titre et un corps. Seul le propriétaire peut lire ou muter ses lignes.

Overview

1. Create schema     →  src/lib/db/schema/notes.ts
2. Export schema     →  src/lib/db/schema/schema.ts
3. Generate migration →  pnpm db:generate
4. Apply migration   →  pnpm db:migrate
5. Create queries    →  src/features/notes/queries.ts

Step 1 — Create schema

Créez src/lib/db/schema/notes.ts :

import { index, pgTable, text, timestamp } from "drizzle-orm/pg-core";

import { user } from "./auth";

export const notes = pgTable(
  "notes",
  {
    id: text("id").primaryKey(),
    userId: text("user_id")
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
    title: text("title").notNull(),
    body: text("body").notNull().default(""),
    createdAt: timestamp("created_at").defaultNow().notNull(),
    updatedAt: timestamp("updated_at")
      .defaultNow()
      .$onUpdate(() => new Date())
      .notNull(),
  },
  (table) => [index("notes_userId_idx").on(table.userId)],
);

Points clés :

  • userId — colonne d'ownership avec cascade delete (quand un utilisateur est supprimé, ses notes le sont aussi)
  • Text primary key — générez les IDs dans le code applicatif (ex. crypto.randomUUID()), comme pour les tables existantes
  • Index on user_id — requis pour les requêtes list-by-owner

Step 2 — Export schema

Ajoutez l'export dans src/lib/db/schema/schema.ts :

export * from "./auth";
export * from "./links";
export * from "./files";
export * from "./user-preferences";
export * from "./notes";   // add this line

Drizzle Kit ne voit que les tables exportées depuis ce barrel.


Step 3 — Generate migration

Assurez-vous que DATABASE_URL est défini dans .env, puis lancez :

pnpm db:generate

Drizzle Kit crée un nouveau dossier sous drizzle/migrations/ avec un SQL similaire à :

CREATE TABLE "notes" (
  "id" text PRIMARY KEY NOT NULL,
  "user_id" text NOT NULL,
  "title" text NOT NULL,
  "body" text DEFAULT '' NOT NULL,
  "created_at" timestamp DEFAULT now() NOT NULL,
  "updated_at" timestamp DEFAULT now() NOT NULL
);

ALTER TABLE "notes" ADD CONSTRAINT "notes_user_id_user_id_fk"
  FOREIGN KEY ("user_id") REFERENCES "public"."user"("id")
  ON DELETE cascade ON UPDATE no action;

CREATE INDEX "notes_userId_idx" ON "notes" USING btree ("user_id");

Relisez le SQL généré avant d'appliquer. Si quelque chose semble incorrect, corrigez le fichier de schéma, supprimez le dossier de migration non appliqué et relancez db:generate.

Motoko Base active RLS sur toutes les tables publiques. Après génération de la migration, ajoutez des instructions RLS au même migration.sql (ou à une migration de suivi) :

ALTER TABLE "notes" ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON TABLE "notes" FROM anon, authenticated;

Cela correspond au motif dans drizzle/migrations/20260822044000_enable_rls/migration.sql. L'app Next.js se connecte via le rôle du pooler et contourne RLS ; les rôles API Supabase anon/authenticated ne peuvent pas lire les lignes.


Step 4 — Apply migration

pnpm db:migrate

Vérifiez avec Drizzle Studio :

pnpm db:studio

Ouvrez la table notes et confirmez que les colonnes correspondent à votre schéma.

Si migrate échoue via le pooler Supabase, basculez temporairement DATABASE_URL vers la connexion direct (port 5432), lancez migrate, puis revenez. Voir Migrations.


Step 5 — Create queries

Créez les requêtes de feature dans src/features/notes/queries.ts. Les lectures et écritures restent dans le dossier feature — pas dans les composants de route.

import { and, desc, eq } from "drizzle-orm";

import { db } from "@/lib/db";
import { notes } from "@/lib/db/schema/notes";

export type NoteItem = {
  id: string;
  title: string;
  body: string;
  createdAt: Date;
};

function toNoteItem(row: typeof notes.$inferSelect): NoteItem {
  return {
    id: row.id,
    title: row.title,
    body: row.body,
    createdAt: row.createdAt,
  };
}

export async function listNotesForUser(userId: string): Promise<NoteItem[]> {
  const rows = await db
    .select()
    .from(notes)
    .where(eq(notes.userId, userId))
    .orderBy(desc(notes.createdAt));

  return rows.map(toNoteItem);
}

export async function findOwnedNote(userId: string, id: string) {
  const [row] = await db
    .select()
    .from(notes)
    .where(and(eq(notes.id, id), eq(notes.userId, userId)))
    .limit(1);

  return row ?? null;
}

export async function insertNote(input: {
  id: string;
  userId: string;
  title: string;
  body: string;
}) {
  const [row] = await db
    .insert(notes)
    .values({
      id: input.id,
      userId: input.userId,
      title: input.title,
      body: input.body,
    })
    .returning();

  return row;
}

export async function deleteOwnedNote(userId: string, id: string) {
  const [row] = await db
    .delete(notes)
    .where(and(eq(notes.id, id), eq(notes.userId, userId)))
    .returning({ id: notes.id });

  return row ?? null;
}

Wire it into the app

Suivez le même motif que Link Manager :

  1. Zod schemas — src/features/notes/schemas.ts pour la validation d'entrée des actions
  2. Server Actions — src/features/notes/actions.ts avec "use server", contrôle de session, parse Zod, appels de queries
  3. UI — src/features/notes/components/notes-page.tsx
  4. Route — src/app/dashboard/notes/page.tsx récupère via queries et rend le composant
  5. Navigation — ajoutez un élément dans src/features/dashboard/config/nav.ts

Dérivez toujours userId de la session — ne faites jamais confiance aux IDs utilisateur fournis par le client :

const session = await getCachedSession();
if (!session?.user?.id) {
  return { ok: false, error: "You must be signed in." };
}
const userId = session.user.id;

Checklist

  • Schema file in src/lib/db/schema/
  • Exported from schema.ts
  • pnpm db:generate — migration SQL reviewed
  • RLS + REVOKE added for Supabase (if using Supabase)
  • pnpm db:migrate — applied successfully
  • Queries in src/features/<name>/queries.ts
  • Ownership filter on every mutation (id + userId)
  • Migration folder committed to git

Next steps

Migrations — Référence des commandes et dépannage.

Database — Architecture, conventions et emplacement des fichiers.

Project structure — Organisation des dossiers feature et règles de base.