Backend setup
Database
Ko-Lab needs PostgreSQL 14+.
Docker
docker run --name kolab-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=kolab \
-p 5432:5432 -d postgres:16
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/kolab
Local install
sudo -u postgres createdb kolab
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
Prisma
# From the repo root — generates the client
npm run db:web
# From apps/web
npx prisma migrate dev # apply migrations (dev)
npx prisma migrate deploy # apply migrations (production)
npx prisma studio # browse data
npx prisma db push # prototype without a migration
:::tip Regenerate after every schema change
npx prisma generate (or npm run db:web) must run after editing
schema.prisma, or TypeScript will not see your new fields.
:::
Migrations
Nine migrations live in apps/web/prisma/migrations/, plus an archived set in
_migrations_archive_2026-07/.
Creating one:
cd apps/web
npx prisma migrate dev --name add_group_lock_field
Review the generated SQL before committing. Prisma will happily write a destructive migration if a change implies one.
Seeds
cd apps/web
npx tsx prisma/seed-call.ts # call system fixtures
node prisma/seedPlanningDemo.mjs # demo planning board
npx tsx prisma/backfill-planning.ts # backfill existing boards
Running
cd apps/web && npm run dev
API routes are served from the same origin as the UI, under /api/*.
Adding a route
- Create
app/api/<name>/route.ts - Export the verbs you support:
GET,POST,PATCH,DELETE - Validate inputs before touching the database
- Authenticate via
lib/apiAuth.ts— do not trust auserIdfrom the query string for anything privileged - Return
{ success: boolean, … } - Add a test under
__tests__/api/
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: Request) {
const body = await req.json();
if (!body.groupId) {
return NextResponse.json(
{ success: false, error: "groupId required" },
{ status: 400 },
);
}
try {
const result = await prisma.group.update({
where: { id: body.groupId },
data: { name: body.name },
});
return NextResponse.json({ success: true, result });
} catch (err) {
console.error("update failed:", err);
return NextResponse.json(
{ success: false, error: "Internal error" },
{ status: 500 },
);
}
}
:::danger Authorization is not automatic
Several existing routes accept a userId query parameter and trust it. When
writing new privileged routes, derive the caller's identity from the session
instead. See Security.
:::
Storage
Uploads go through lib/s3.ts, which targets any S3-compatible endpoint.
Configure per Credentials.
Two buckets: S3_BUCKET for durable objects (avatars), DRAFT_BUCKET for
unsaved editor drafts.