Dokumentation
Neue Tabelle hinzufügen
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.tsStep 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 lineDrizzle 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:generateDrizzle 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.
Supabase RLS (recommended)
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:migrateMit 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:
- Zod schemas —
src/features/notes/schemas.tsfür Action-Eingabevalidierung - Server Actions —
src/features/notes/actions.tsmit"use server", Session-Check, Zod-Parse, Query-Aufrufe - UI —
src/features/notes/components/notes-page.tsx - Route —
src/app/dashboard/notes/page.tsxlädt über Queries und rendert die Komponente - Navigation — Eintrag in
src/features/dashboard/config/nav.tshinzufü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.