Dokumentation

Neue Tabelle hinzufügen

Schritt-für-Schritt-Tutorial — Drizzle-Schema erstellen, exportieren, Migration erzeugen, anwenden und Queries in Motoko Base schreiben.

Öffnen inChatGPT (öffnet in neuem Tab)Claude (öffnet in neuem Tab)Cursor (öffnet in neuem Tab)Schritt-für-Schritt-Tutorial — Drizzle-Schema erstellen, exportieren, Migration erzeugen, anwenden und Queries in Motoko Base schreiben.

Dieser Walkthrough fügt eine Tabelle notes hinzu — eine einfache benutzerbezogene Ressource, ähnlich wie Link Manager. Passe dieselben Schritte für jedes neue Feature an.

Ziel: Jeder Benutzer kann Notizen mit Titel und Inhalt speichern. Nur der Eigentümer kann seine Zeilen lesen oder mutieren.

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

Erstelle 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)],
);

Wichtige Punkte:

  • userId — Ownership-Spalte mit Cascade-Delete (wird ein Benutzer gelöscht, gehen auch seine Notizen)
  • Text primary key — IDs im Anwendungscode erzeugen (z. B. crypto.randomUUID()), wie bei bestehenden Tabellen
  • Index on user_id — erforderlich für List-by-Owner-Queries

Step 2 — Export schema

Füge den Export in src/lib/db/schema/schema.ts hinzu:

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

Drizzle Kit sieht nur Tabellen, die aus diesem Barrel exportiert werden.


Step 3 — Generate migration

Stelle sicher, dass DATABASE_URL in .env gesetzt ist, und führe dann aus:

pnpm db:generate

Drizzle Kit erstellt einen neuen Ordner unter drizzle/migrations/ mit SQL ähnlich wie:

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");

Prüfe das erzeugte SQL vor dem Anwenden. Wenn etwas falsch aussieht, korrigiere die Schema-Datei, lösche den nicht angewendeten Migrationsordner und führe db:generate erneut aus.

Motoko Base aktiviert RLS auf allen öffentlichen Tabellen. Nach dem Generieren der Migration füge RLS-Statements derselben migration.sql hinzu (oder einer Folge-Migration):

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

Das entspricht dem Muster in drizzle/migrations/20260822044000_enable_rls/migration.sql. Die Next.js-App verbindet sich über die Pooler-Rolle und umgeht RLS; anon/authenticated-Supabase-API-Rollen können keine Zeilen lesen.


Step 4 — Apply migration

pnpm db:migrate

Mit Drizzle Studio prüfen:

pnpm db:studio

Öffne die Tabelle notes und bestätige, dass die Spalten zu deinem Schema passen.

Wenn migrate über den Supabase-Pooler scheitert, wechsle DATABASE_URL vorübergehend zur Direct-Connection (Port 5432), führe migrate aus und wechsle zurück. Siehe Migrations.


Step 5 — Create queries

Erstelle Feature-Queries in src/features/notes/queries.ts. Lese- und Schreibzugriffe bleiben im Feature-Ordner — nicht in Route-Komponenten.

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

Folge demselben Muster wie Link Manager:

  1. Zod schemas — src/features/notes/schemas.ts für Action-Eingabevalidierung
  2. Server Actions — src/features/notes/actions.ts mit "use server", Session-Check, Zod-Parse, Query-Aufrufe
  3. UI — src/features/notes/components/notes-page.tsx
  4. Route — src/app/dashboard/notes/page.tsx lädt über Queries und rendert die Komponente
  5. Navigation — Eintrag in src/features/dashboard/config/nav.ts hinzufügen

Leite userId immer aus der Session ab — vertraue nie clientseitig gelieferten User-IDs:

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 — Befehlsreferenz und Fehlerbehebung.

Database — Architektur, Konventionen und Dateiorte.

Project structure — Feature-Ordnerlayout und Faustregeln.