Imported from nagoodman/code-server-docker (
code-server/AGENTS.md). Install upstream withnpx skills add nagoodman/code-server-docker --skill code-server. Copyright stays with the author.
AGENTS.md
This file provides guidance to agents when working with code in this repository.
Starting the Server
Development:
cd code-server/nextjs-app
npm run dev
# Access at /proxy/3000 in code-server (NOT localhost:3000 directly)
Production:
docker-compose up -d
# Access code-server at http://localhost:8443
Database Setup:
cd code-server/nextjs-app
npm run db:init # Generate Prisma client and run migrations
Application Architecture
This project consists of two distinct parts:
1. Public Website (/, /content, etc.)
- Routes: Root pages outside
/approute (e.g.,/,/content/page.tsx,/signin,/signup) - Access: Open to all users, no authentication required
- Purpose: Marketing pages, public content, sign-up/sign-in flows
2. Authenticated Application (/app/*)
- Routes: All routes under
/appdirectory (e.g.,/app/page.tsx,/app/dashboard, etc.) - Access: Protected by authentication middleware
- Purpose: User-specific features, dashboards, authenticated functionality
- Protection: Middleware (
middleware.ts:8-18) automatically redirects unauthenticated users to/signin
Route Separation Strategy
The split is enforced at the route level:
- Public routes: Any page NOT under
/appdirectory - Protected routes: Any page under
/appdirectory - Middleware matcher (
middleware.ts:24) only targets/app/:path*
Authentication Integration
Better-Auth with PostgreSQL:
- Configuration:
lib/auth.ts:7-17 - Adapter: Prisma with PostgreSQL (
better-auth/adapters/prisma) - Email/Password: Enabled by default
- Session Management: Cookie-based (
better-auth.session_token)
Sign-up/Sign-in Pages
Location:
- Sign-up:
/signup/page.tsx - Sign-in:
/signin/page.tsx
Unlikely to Need Modification: These pages are already implemented with better-auth and handle:
- Email/password authentication
- Session creation
- Redirect after login (via
callbackUrlquery param) - Form validation and error handling
When you might need to modify:
- Adding OAuth providers (Google, GitHub, etc.)
- Custom branding/styling
- Additional form fields during registration
- Custom redirect logic
Multi-Tenant / Row-Level Security (RLS)
Current Database Schema
The existing Prisma schema (prisma/schema.prisma) has:
Usermodel with sessions and accountsSessionmodel with user relationshipAccountmodel for auth providersVerificationmodel for email verification
Implementing Multi-Tenancy
To add multi-tenant functionality with better-auth and PostgreSQL:
1. Add Organization/Tenant Model
model Organization {
id String @id @default(cuid())
name String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members OrganizationMember[]
// Add your tenant-scoped resources here
posts Post[]
@@map("organization")
}
model OrganizationMember {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role String @default("member") // "owner", "admin", "member"
createdAt DateTime @default(now())
@@unique([organizationId, userId])
@@map("organization_member")
}
// Example tenant-scoped resource
model Post {
id String @id @default(cuid())
title String
content String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId]) // Important for query performance
@@map("post")
}
2. Update User Model
Add the relationship to organizations:
model User {
// ... existing fields
organizations OrganizationMember[]
posts Post[]
}
3. Implement Row-Level Filtering
In API Routes/Server Actions:
// lib/tenant.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function getCurrentOrgId(): Promise<string | null> {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user) return null;
// Get from session, subdomain, or cookie
// This is a simplified example
return session.user.currentOrgId ?? null;
}
// app/api/posts/route.ts
import { prisma } from "@/lib/prisma";
import { getCurrentOrgId } from "@/lib/tenant";
export async function GET() {
const orgId = await getCurrentOrgId();
if (!orgId) return Response.json({ error: "Unauthorized" }, { status: 401 });
// Row-level filtering: ALWAYS filter by organizationId
const posts = await prisma.post.findMany({
where: { organizationId: orgId }
});
return Response.json(posts);
}
4. Middleware Enhancement
Extend middleware.ts to inject tenant context:
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
if (pathname.startsWith("/app")) {
const sessionToken = request.cookies.get("better-auth.session_token");
if (!sessionToken) {
const signInUrl = new URL("/signin", request.url);
signInUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(signInUrl);
}
// Optional: Inject tenant ID from subdomain or path
// const orgSlug = request.headers.get("host")?.split(".")[0];
// const response = NextResponse.next();
// response.headers.set("x-organization-slug", orgSlug);
// return response;
}
return NextResponse.next();
}
5. Database-Level RLS (PostgreSQL)
For strict security, enable PostgreSQL Row-Level Security:
-- Enable RLS on tables
ALTER TABLE "post" ENABLE ROW LEVEL SECURITY;
-- Create policy (execute in PostgreSQL, not Prisma)
CREATE POLICY tenant_isolation ON "post"
USING (organization_id = current_setting('app.current_org_id')::text);
-- In your application, set the org context per query
-- This requires using Prisma raw queries with session variables
Note: Prisma doesn't natively support PostgreSQL RLS. You'll need to:
- Set session variables before queries:
SET app.current_org_id = '...' - Use Prisma middleware to inject
organizationIdfilters automatically - Or use raw SQL for critical operations
6. Best Practices
- Always filter by
organizationIdin every query for tenant-scoped resources - Use Prisma middleware to automatically inject tenant filters
- Index foreign key columns (
organizationId) for performance - Store current organization in session or JWT
- Validate user membership before allowing access to organization data
- Consider using PostgreSQL schemas (one per tenant) for strict isolation at scale
Summary
The existing better-auth + PostgreSQL + Prisma stack is well-suited for multi-tenancy:
- Add
OrganizationandOrganizationMembermodels - Create foreign key relationships to tenant-scoped resources
- Filter all queries by
organizationId(application-level RLS) - Optionally use PostgreSQL RLS for database-level enforcement
- Middleware handles authentication; application code handles tenant scoping
Critical Non-Obvious Configuration
Code-server Proxy Setup (REQUIRED):
- App MUST run from
nextjs-app/directory, not project root - Environment variables in
.envare MANDATORY - dev server won't work correctly without them ASSET_PREFIX=/proxy/3000is required for static assets to load through code-server's proxy- Access app at
/proxy/3000in code-server, NOTlocalhost:3000directly next.config.tsusesassetPrefixbut NOTbasePath(counterintuitive - basePath is empty because proxy handles routing)
Build Configuration
Standalone Output:
next.config.tssetsoutput: 'standalone'for Docker deployment- This creates a self-contained build in
.next/standalone/(not typical Next.js output structure)
Tailwind CSS v4
- Uses
@tailwindcss/postcssplugin (v4 architecture, not traditional tailwind.config.js) - Configuration is in
postcss.config.mjs, not a separate Tailwind config file - No
tailwind.config.jsfile exists - this is intentional for v4
TypeScript Paths
- Path alias
@/*maps to project root (./*), not./src/*- seetsconfig.json - Import from root:
import { X } from '@/app/component'not@/src/app/component