Backend & API
Next.js API routes, authentication via NextAuth, RBAC authorization via CASL, REST endpoints organized by domain.
Architecture
CDT's backend is a single Core API running inside Next.js API routes — no separate server process. The Core API hosts business logic and CRUD endpoints; Authentication and Authorization are cross-cutting services it calls on every request. Routes are organized by domain under src/app/api/:
src/app/api/
├── buildings/route.ts
├── sites/route.ts
├── sensors/route.ts
├── files/route.ts
├── comments/route.ts
├── organizations/route.ts
├── users/route.ts
├── infrastructure/route.ts
├── openDataPortals/route.ts
└── ...
Route Conventions
Each route uses the shared getRequestAbility helper to authenticate and load the caller's CASL ability in one step. The helper calls auth() internally and returns the user's id, their organization id, and an ability built from the role's permissions — or an errorResponse if the request is unauthenticated.
// src/app/api/buildings/route.ts
import { NextResponse } from 'next/server';
import { getRequestAbility, forbidAction } from '@/lib/permissions';
import prisma from '@/lib/prisma';
export async function GET() {
const { errorResponse, organizationId, ability } = await getRequestAbility();
if (errorResponse) return errorResponse;
if (!ability.can('read', 'Building')) return forbidAction();
const buildings = await prisma.building.findMany({
where: { organizationId },
});
return NextResponse.json(buildings);
}
The same shape applies to POST, PATCH, and DELETE — call getRequestAbility, check the relevant (action, subject) against ability.can(...), then mutate scoped by the caller's organizationId.
Authentication
NextAuth v5 manages user sessions. The session carries the user's id and their organization id, which getRequestAbility reads on every request to scope queries and load the right CASL ability.
Sign-in requires a multi-factor code delivered by email. MFA codes expire after 5 minutes. Sessions are valid for 8 hours and are not refreshed on activity — users are signed out at expiry and must sign in again.
Authorization (RBAC via CASL)
Role-Based Access Control (RBAC) is enforced by CASL consistently on server + client. Each role owns a set of (action, subject) permission pairs that are loaded for the caller on every request and compiled into an Ability.
Build an ability from a role's permissions
// src/lib/permissions.ts
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
export function buildAbilityFromPermissions(perms) {
const { can, build } = new AbilityBuilder(createMongoAbility);
for (const p of perms) can(p.action, p.subject);
return build();
}
API check
getRequestAbility returns the ability for the current request. Routes use it to gate every action:
const { errorResponse, ability } = await getRequestAbility();
if (errorResponse) return errorResponse;
if (!ability.can('update', 'Building')) return forbidAction();
Frontend check
The frontend builds the same ability from the session and uses it to hide controls the user cannot use:
{ability.can('update', 'Building') && <EditButton />}
{ability.can('delete', 'Building') && <DeleteButton />}
See Authorization → Permission reference for the full role/permission matrix.
Error Handling
Standard response format:
{
"error": "Validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
}
Common status codes:
- 400 — Validation error (bad input)
- 401 — Not authenticated
- 403 — Authenticated but not authorized
- 404 — Resource not found
- 429 — Rate limit exceeded
- 500 — Server error
Middleware
A Next.js middleware runs on every request to enforce authentication before any route handler executes. Unauthenticated requests are redirected to the sign-in page; the route handler itself never sees them.