nextjs-fullstack-bootstrap
Skill4 starsChanged 3 months agoMITView on GitHub
Add full-stack backend infrastructure to a Next.js app (Drizzle, Postgres, Redis, BullMQ, Auth.js).
- Reads credentials
- Deletes or force-pushes
In this repository53 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- llms.txtllms.txt
- actionableSkill
- astro-bootstrapSkill
- astro-patternsSkill
- autopilotSkill
- axon-code-intelSkill
- brainstormingSkill
- commit-conventionsSkill
- cross-repo-contextSkill
- django-bootstrapSkill
- django-patternsSkill
- executeSkill
- find-sessionSkill
- flutter-bootstrapSkill
- flutter-patternsSkill
- full-featureSkill
- intelSkill
- nextjs-bootstrapSkill
- nextjs-fullstack-patternsSkill
- nextjs-mui-bootstrapSkill
- nextjs-patternsSkill
- nextjs-shadcn-bootstrapSkill
- orchestratorSkill
- phase-breakdownSkill
- plan-and-scopeSkill
- prdSkill
- reactSkill
- recallSkill
- reflectSkill
- relaySkill
- rememberSkill
- scaffoldSkill
- scratchSkill
- tailwindSkill
- ux-specSkill
- ux-to-promptsSkill
- verify-astroSkill
- verify-djangoSkill
- verify-flutterSkill
- verify-nextjs-fullstackSkill
- verify-nextjs-muiSkill
- verify-nextjs-shadcnSkill
- verify-nextjsSkill
- verifySkill
- visual-feedbackSkill
- work-disciplineSkill
- worktreesSkill
- wrapSkill
- write-as-friendSkill
- write-as-humanSkill
- writing-clearlySkill
- writing-skillsSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 4 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Ask the user for these values (provide defaults where shown):
- Very specific: line 41
Verify the base Next.js project exists with src/app/ structure
- Distinctive: line 43
Create backend files from templates below (respecting blueprint tier tags)
- Very specific: line 44
Update docker-compose.yml with database and Redis services
- Very specific: line 45
Update Makefile with database and queue commands
- Very specific: line 46
Update .env.local with new environment variables
- Distinctive: line 1073
After completing a backend feature, generate an integration summary:
- Distinctive: line 1077
Document error response shapes
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Next.js Full-Stack Bootstrap
On-demand command for adding backend infrastructure to a Next.js project — Drizzle ORM, Postgres, Redis, BullMQ, and Auth.js. Supports three blueprint tiers for right-sized infrastructure.
This bootstrap adds backend pieces to an existing Next.js project. Run the appropriate frontend bootstrap first (
/nextjs-bootstrap, etc.) to create the base project, then run this to add the backend layer.
MANDATORY: This project runs entirely in Docker. All Docker service additions (db, redis, worker) MUST be added to the existing docker-compose.yml. The Makefile is the sole interface — never run
npmornpxdirectly on the host.
Before You Start
Ask the user for these values (provide defaults where shown):
| Placeholder | Description | Example |
{blueprint} | Blueprint tier: minimal, standard, or full | standard |
{app-name} | Existing project directory name | my-app |
{compose-project} | Docker Compose project name (kebab-case) | my-app |
{db_name} | Postgres database name (snake_case) | my_app |
{host_port} | Host port for Next.js (should match existing) | 3000 |
Blueprint Tiers
| Tier | Name | Includes | Use when |
| 1 | minimal | Drizzle + Postgres + Docker services. No Redis, no auth, no queues, no Sentry. | Simple full-stack apps, prototypes, no user accounts |
| 2 | standard | Minimal + Redis cache + Auth.js + Sentry. No BullMQ, S3, email. | Most production apps with auth |
| 3 | full | Standard + BullMQ workers + S3/R2 + email. | Background tasks, file uploads, notifications |
Only generate files and sections marked for your blueprint tier. Sections are tagged
[ALL],[STANDARD+], or[FULL]. Generate[ALL]always,[STANDARD+]for standard and full,[FULL]only for full.
Bootstrapping Steps
- Verify the base Next.js project exists with
src/app/structure - Install dependencies (add to
package.json) - Create backend files from templates below (respecting blueprint tier tags)
- Update
docker-compose.ymlwith database and Redis services - Update
Makefilewith database and queue commands - Update
.env.localwith new environment variables make build && make upmake db-generate && make db-migrate- Verify at
http://localhost:{host_port}
New Files to Create
Directory Structure (additions to existing project)
[MINIMAL]:
src/
├── lib/
│ └── db/
│ ├── index.ts # Drizzle client
│ ├── schema/
│ │ ├── index.ts # Re-exports all tables
│ │ └── posts.ts # Example table
│ └── migrations/ # Generated by drizzle-kit
├── server/
│ ├── actions/
│ │ └── posts.ts # Example server actions
│ └── services/
│ └── posts.ts # Example service
├── app/
│ └── api/
│ └── v1/
│ └── posts/
│ └── route.ts # Example route handler
drizzle.config.ts[STANDARD] — adds auth, redis, middleware:
src/
├── lib/
│ ├── auth.ts # Auth.js configuration
│ ├── redis.ts # Redis client
│ └── db/
│ ├── index.ts
│ ├── schema/
│ │ ├── index.ts
│ │ ├── users.ts # Auth.js user/account/session tables
│ │ └── posts.ts
│ └── migrations/
├── server/
│ ├── actions/
│ │ └── posts.ts
│ └── services/
│ └── posts.ts
├── app/
│ ├── api/
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts # Auth.js route handler
│ │ └── v1/
│ │ └── posts/
│ │ └── route.ts
│ └── (auth)/
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
├── types/
│ └── next-auth.d.ts # Session type extensions
middleware.ts
drizzle.config.ts[FULL] — adds BullMQ, storage service:
src/
├── lib/
│ ├── auth.ts
│ ├── redis.ts
│ ├── db/
│ │ ├── index.ts
│ │ ├── schema/
│ │ │ ├── index.ts
│ │ │ ├── users.ts
│ │ │ └── posts.ts
│ │ └── migrations/
│ └── queue/
│ ├── client.ts # Shared BullMQ connection
│ ├── queues.ts # Queue definitions
│ └── workers.ts # Worker processors
├── server/
│ ├── actions/
│ │ └── posts.ts
│ └── services/
│ ├── posts.ts
│ ├── storage.ts # S3/R2 file uploads
│ └── email.ts # Email via Resend
├── app/
│ ├── api/
│ │ ├── auth/
│ │ │ └── [...nextauth]/
│ │ │ └── route.ts
│ │ └── v1/
│ │ └── posts/
│ │ └── route.ts
│ └── (auth)/
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
├── types/
│ └── next-auth.d.ts
middleware.ts
drizzle.config.tsFile Templates
drizzle.config.ts [ALL]
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/lib/db/schema/index.ts",
out: "./src/lib/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});src/lib/db/index.ts [ALL]
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool, { schema });src/lib/db/schema/index.ts [ALL]
[MINIMAL]:
export * from "./posts";[STANDARD+]:
export * from "./users";
export * from "./posts";src/lib/db/schema/posts.ts [ALL]
import { pgTable, serial, text, timestamp, uuid, index } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { users } from "./users";
export const posts = pgTable(
"posts",
{
id: serial("id").primaryKey(),
uuid: uuid("uuid").defaultRandom().unique().notNull(),
// Owner — ties each post to the authenticated user (STANDARD+ auth tier).
// Minimal (no-auth) blueprint: drop this column and the auth()/user scoping
// shown in the service, actions, and route below; posts are then unowned.
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
title: text("title").notNull(),
content: text("content"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date()),
},
(t) => [index("posts_user_id_idx").on(t.userId)]
);
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;src/lib/db/schema/users.ts [STANDARD+]
import {
pgTable,
serial,
text,
timestamp,
uuid,
integer,
primaryKey,
} from "drizzle-orm/pg-core";
import type { AdapterAccountType } from "next-auth/adapters";
export const users = pgTable("users", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique().notNull(),
emailVerified: timestamp("email_verified", { mode: "date" }),
image: text("image"),
hashedPassword: text("hashed_password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date()),
});
export const accounts = pgTable(
"accounts",
{
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccountType>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => [
primaryKey({ columns: [account.provider, account.providerAccountId] }),
]
);
export const sessions = pgTable("sessions", {
sessionToken: text("session_token").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
});
export const verificationTokens = pgTable(
"verification_tokens",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => [primaryKey({ columns: [vt.identifier, vt.token] })]
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;src/lib/auth.ts [STANDARD+]
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";
import bcrypt from "bcryptjs";
import { eq } from "drizzle-orm";
import { users } from "@/lib/db/schema";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
// Credentials logins never create database session rows in Auth.js —
// the JWT strategy is required for them to work, even with an adapter
session: { strategy: "jwt" },
pages: {
signIn: "/login",
},
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await db.query.users.findFirst({
where: eq(users.email, credentials.email as string),
});
if (!user?.hashedPassword) return null;
const isValid = await bcrypt.compare(
credentials.password as string,
user.hashedPassword
);
if (!isValid) return null;
return { id: user.id, name: user.name, email: user.email };
},
}),
// Add OAuth providers here:
// Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET! }),
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id;
return token;
},
session({ session, token }) {
session.user.id = token.id as string;
return session;
},
},
});src/app/api/auth/[...nextauth]/route.ts [STANDARD+]
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;src/middleware.ts [STANDARD+]
import { auth } from "@/lib/auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAuthPage =
req.nextUrl.pathname.startsWith("/login") ||
req.nextUrl.pathname.startsWith("/register");
const isApiAuth = req.nextUrl.pathname.startsWith("/api/auth");
const isPublic =
req.nextUrl.pathname === "/" || req.nextUrl.pathname.startsWith("/api/v1");
// Allow auth API routes and public pages
if (isApiAuth || isPublic) return;
// Redirect logged-in users away from auth pages
if (isAuthPage && isLoggedIn) {
return Response.redirect(new URL("/dashboard", req.nextUrl));
}
// Redirect unauthenticated users to login
if (!isAuthPage && !isLoggedIn) {
return Response.redirect(new URL("/login", req.nextUrl));
}
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)"],
};src/types/next-auth.d.ts [STANDARD+]
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}src/lib/redis.ts [STANDARD+]
import Redis from "ioredis";
const globalForRedis = globalThis as unknown as { redis: Redis | undefined };
export const redis =
globalForRedis.redis ??
new Redis(process.env.REDIS_URL ?? "redis://localhost:6379");
if (process.env.NODE_ENV !== "production") globalForRedis.redis = redis;src/lib/queue/client.ts [FULL]
import Redis from "ioredis";
// Shared connection for BullMQ — separate from the cache client
// BullMQ requires maxRetriesPerRequest: null
export const queueConnection = new Redis(
process.env.REDIS_URL ?? "redis://localhost:6379",
{ maxRetriesPerRequest: null }
);src/lib/queue/queues.ts [FULL]
import { Queue } from "bullmq";
import { queueConnection } from "./client";
export const emailQueue = new Queue("email-notifications", {
connection: queueConnection,
defaultJobOptions: {
attempts: 3,
backoff: { type: "exponential", delay: 1000 },
removeOnComplete: { count: 100 },
removeOnFail: { count: 500 },
},
});
// Add more queues as needed:
// export const dataProcessingQueue = new Queue("data-processing", { ... });src/lib/queue/workers.ts [FULL]
import { Worker } from "bullmq";
import { queueConnection } from "./client";
// Email notification worker
const emailWorker = new Worker(
"email-notifications",
async (job) => {
const { to, subject, body } = job.data;
// Call the email service here
console.log(`Processing email job ${job.id}: ${subject} -> ${to}`);
},
{ connection: queueConnection, concurrency: 5 }
);
emailWorker.on("completed", (job) => {
console.log(`Job ${job.id} completed`);
});
emailWorker.on("failed", (job, err) => {
console.error(`Job ${job?.id} failed:`, err.message);
});
export { emailWorker };src/server/services/posts.ts [ALL]
import { db } from "@/lib/db";
import { posts, type NewPost, type Post } from "@/lib/db/schema";
import { and, count, eq } from "drizzle-orm";
// Queries are scoped to the owning user (STANDARD+ auth tier). Minimal
// (no-auth) blueprint: drop the userId parameters and the userId filters.
export async function listPosts(
userId: string,
{ page, pageSize }: { page: number; pageSize: number }
): Promise<{ results: Post[]; count: number }> {
const offset = (page - 1) * pageSize;
const [results, totals] = await Promise.all([
db.query.posts.findMany({
where: eq(posts.userId, userId),
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
limit: pageSize,
offset,
}),
db.select({ total: count() }).from(posts).where(eq(posts.userId, userId)),
]);
return { results, count: totals[0].total };
}
export async function getPost(
uuid: string,
userId: string
): Promise<Post | undefined> {
return db.query.posts.findFirst({
where: and(eq(posts.uuid, uuid), eq(posts.userId, userId)),
});
}
export async function createPost(data: NewPost): Promise<Post> {
const [post] = await db.insert(posts).values(data).returning();
return post;
}
export async function updatePost(
uuid: string,
userId: string,
data: Partial<NewPost>
): Promise<Post | undefined> {
const [post] = await db
.update(posts)
.set(data)
.where(and(eq(posts.uuid, uuid), eq(posts.userId, userId)))
.returning();
return post;
}
export async function deletePost(uuid: string, userId: string): Promise<void> {
await db.delete(posts).where(and(eq(posts.uuid, uuid), eq(posts.userId, userId)));
}src/server/actions/posts.ts [ALL]
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import * as postService from "@/server/services/posts";
import { z } from "zod";
type ActionResult<T = void> =
| { success: true; data: T }
| { success: false; error: string };
const createPostSchema = z.object({
title: z.string().trim().min(1, "Title is required"),
content: z.string().optional(),
});
// auth() gates every mutation (STANDARD+ auth tier). Minimal (no-auth)
// blueprint: drop the session checks and the userId argument.
export async function createPost(
formData: FormData
): Promise<ActionResult<{ uuid: string }>> {
const session = await auth();
if (!session?.user?.id) {
return { success: false, error: "Unauthorized" };
}
const parsed = createPostSchema.safeParse({
title: formData.get("title"),
content: formData.get("content") ?? undefined,
});
if (!parsed.success) {
return { success: false, error: parsed.error.issues[0].message };
}
const post = await postService.createPost({
...parsed.data,
userId: session.user.id,
});
revalidatePath("/posts");
return { success: true, data: { uuid: post.uuid } };
}
export async function deletePost(uuid: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user?.id) {
return { success: false, error: "Unauthorized" };
}
await postService.deletePost(uuid, session.user.id);
revalidatePath("/posts");
return { success: true, data: undefined };
}src/app/api/v1/posts/route.ts [ALL]
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import * as postService from "@/server/services/posts";
import { z } from "zod";
const createPostSchema = z.object({
title: z.string().min(1, "Title is required"),
content: z.string().optional(),
});
// middleware treats /api/v1/* as public, so each handler authenticates itself
// (STANDARD+ auth tier). Minimal (no-auth) blueprint: drop the auth() guards
// and the userId scoping.
export async function GET(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Canonical pagination contract: page/page_size params → {page, count, num_pages, results}
const { searchParams } = new URL(request.url);
const page = Math.max(1, Number(searchParams.get("page")) || 1);
const pageSize = Math.min(
100,
Math.max(1, Number(searchParams.get("page_size")) || 20)
);
const { results, count } = await postService.listPosts(session.user.id, {
page,
pageSize,
});
return NextResponse.json({
page,
count,
num_pages: Math.max(1, Math.ceil(count / pageSize)),
results,
});
}
export async function POST(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const result = createPostSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: "Validation failed", details: z.flattenError(result.error).fieldErrors },
{ status: 400 }
);
}
const post = await postService.createPost({
...result.data,
userId: session.user.id,
});
return NextResponse.json(post, { status: 201 });
}src/server/services/storage.ts [FULL]
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: "auto",
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
});
const bucket = process.env.R2_BUCKET_NAME!;
export async function uploadFile(
key: string,
body: Buffer | ReadableStream,
contentType: string
): Promise<string> {
await s3.send(
new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType })
);
if (process.env.R2_CUSTOM_DOMAIN) {
return `https://${process.env.R2_CUSTOM_DOMAIN}/${key}`;
}
return key;
}
export async function deleteFile(key: string): Promise<void> {
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
}
export async function getSignedDownloadUrl(
key: string,
expiresIn = 3600
): Promise<string> {
return getSignedUrl(
s3,
new GetObjectCommand({ Bucket: bucket, Key: key }),
{ expiresIn }
);
}src/server/services/email.ts [FULL]
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
interface SendEmailOptions {
to: string | string[];
subject: string;
html: string;
from?: string;
}
export async function sendEmail({
to,
subject,
html,
from = process.env.DEFAULT_FROM_EMAIL ?? "noreply@example.com",
}: SendEmailOptions) {
const { data, error } = await resend.emails.send({
from,
to: Array.isArray(to) ? to : [to],
subject,
html,
});
if (error) {
throw new Error(`Failed to send email: ${error.message}`);
}
return data;
}Docker Services (add to existing docker-compose.yml)
[MINIMAL] — add db service
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB={db_name}
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
volumes:
postgres_data:Also update the existing app service:
app:
depends_on:
- db
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}[STANDARD] — add db + redis
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB={db_name}
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
redis:
image: redis:7
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Also update the existing app service:
app:
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}
- REDIS_URL=redis://redis:6379/0[FULL] — add db + redis + worker
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB={db_name}
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
redis:
image: redis:7
volumes:
- redis_data:/data
worker:
build:
context: .
dockerfile: Dockerfile.dev
command: npx tsx --watch src/lib/queue/workers.ts
volumes:
- .:/app
- /app/node_modules
depends_on:
- redis
- db
env_file:
- .env.local
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}
- REDIS_URL=redis://redis:6379/0
volumes:
postgres_data:
redis_data:Also update the existing app service:
app:
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}
- REDIS_URL=redis://redis:6379/0Makefile Additions (append to existing Makefile)
[ALL] — Database commands
# ============================================================================
# Database (Drizzle)
# ============================================================================
.PHONY: db-generate db-migrate db-push db-studio db-seed dbshell dbreset
db-generate:
$(DOCKER_COMPOSE) exec app npx drizzle-kit generate
db-migrate:
$(DOCKER_COMPOSE) exec app npx drizzle-kit migrate
db-push:
$(DOCKER_COMPOSE) exec app npx drizzle-kit push
db-studio:
$(DOCKER_COMPOSE) exec app npx drizzle-kit studio
dbshell:
$(DOCKER_COMPOSE) exec db psql -U postgres -d {db_name}
dbreset:
@echo "WARNING: This will delete the database!"
@read -p "Are you sure? [y/N] " confirm && [ "$$confirm" = "y" ]
$(DOCKER_COMPOSE) exec db psql -U postgres -c "DROP DATABASE IF EXISTS {db_name};"
$(DOCKER_COMPOSE) exec db psql -U postgres -c "CREATE DATABASE {db_name};"
$(DOCKER_COMPOSE) exec app npx drizzle-kit migrate[FULL] — Queue commands
# ============================================================================
# Queue (BullMQ)
# ============================================================================
.PHONY: worker logs-worker
worker:
$(DOCKER_COMPOSE) exec worker npx tsx src/lib/queue/workers.ts
logs-worker:
$(DOCKER_COMPOSE) logs -f workerPackage Dependencies (add to existing package.json)
[MINIMAL]
{
"dependencies": {
"drizzle-orm": "^0.45",
"pg": "^8.13",
"zod": "^4.4"
},
"devDependencies": {
"drizzle-kit": "^0.31",
"@types/pg": "^8.11"
}
}[STANDARD] (adds auth, redis, bcrypt)
{
"dependencies": {
"drizzle-orm": "^0.45",
"pg": "^8.13",
"zod": "^4.4",
"next-auth": "5.0.0-beta.31",
"@auth/drizzle-adapter": "^1.11",
"bcryptjs": "^3.0",
"ioredis": "^5.4"
},
"devDependencies": {
"drizzle-kit": "^0.31",
"@types/pg": "^8.11"
}
}[FULL] (adds bullmq, s3, resend)
{
"dependencies": {
"drizzle-orm": "^0.45",
"pg": "^8.13",
"zod": "^4.4",
"next-auth": "5.0.0-beta.31",
"@auth/drizzle-adapter": "^1.11",
"bcryptjs": "^3.0",
"ioredis": "^5.4",
"bullmq": "^5.30",
"@aws-sdk/client-s3": "^3.700",
"@aws-sdk/s3-request-presigner": "^3.700",
"resend": "^4.1"
},
"devDependencies": {
"drizzle-kit": "^0.31",
"@types/pg": "^8.11",
"tsx": "^4.19"
}
}Environment Variables (add to existing .env.local)
[MINIMAL]
# Database
DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}[STANDARD] (adds redis, auth)
# Database
DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}
# Redis
REDIS_URL=redis://redis:6379/0
# Auth.js
AUTH_SECRET=generate-a-random-secret-here
AUTH_URL=http://localhost:{host_port}
# Error Tracking
SENTRY_DSN=[FULL] (adds storage, email)
# Database
DATABASE_URL=postgres://postgres:postgres@db:5432/{db_name}
# Redis
REDIS_URL=redis://redis:6379/0
# Auth.js
AUTH_SECRET=generate-a-random-secret-here
AUTH_URL=http://localhost:{host_port}
# Storage (Cloudflare R2)
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=
R2_CUSTOM_DOMAIN=
# Email (Resend)
RESEND_API_KEY=
DEFAULT_FROM_EMAIL=noreply@example.com
# Error Tracking
SENTRY_DSN=Integration Summaries
After completing a backend feature, generate an integration summary:
- List all new/modified endpoints with method, path, auth requirements
- Include request/response shapes as JSON examples
- Note any pagination, filtering, or ordering parameters
- Document error response shapes
- Save to
docs/integration/[feature-name].md
This mirrors the Django convention so frontend consumers have a consistent reference.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

