Skip to main content

Voters (Authorization)

Voters control who can access which routes.

config/voters/ArticleVoter.json
{
"name": "ArticleVoter",
"rules": [
{
"methods": ["GET"],
"resource": "Article"
},
{
"methods": ["POST", "PUT", "DELETE"],
"resource": "Article",
"condition": {
"self": true
}
}
]
}

This means:

  • Anyone can GET articles
  • Only the article's owner can POST, PUT, or DELETE

Rule fields

FieldTypeDescription
methodsHttpMethod[]Allowed HTTP methods
resourcestringModel name (resource being accessed)
condition.selfbooleanUser must be the owner
condition.fieldstringField to compare
condition.operatorstringComparison operator (==, !=, …)
condition.valueanyValue to compare against

Using a voter on a route

Add "voter": "ArticleVoter" (and optionally "auth": true) to a route definition:

{
"method": "DELETE",
"path": "/api/articles/:id",
"auth": true,
"voter": "ArticleVoter"
}

Generated output

src/lib/auth.ts

// Auto-generated by Codabra — do not edit manually
import { NextRequest } from "next/server";

export function getAuthUser(req: NextRequest): string | null {
const auth = req.headers.get("authorization");
if (!auth || !auth.startsWith("Bearer ")) return null;
return auth.slice(7) || null;
}

src/lib/voters/ArticleVoter.ts

// Auto-generated by Codabra — do not edit manually
import { NextRequest } from "next/server";

export function checkArticleVoter(
_req: NextRequest,
method: string,
resource: unknown,
userId: string | null,
): boolean {
if (["GET"].includes(method as never)) {
return true;
}
if (["POST", "PUT", "DELETE"].includes(method as never)) {
return resource != null && (resource as Record<string, unknown>)["userId"] === userId;
}
return false;
}

Route handler (excerpt)

export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) {
const id = params.id;
const userId = getAuthUser(_req);
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const resource = await db
.select()
.from(articles)
.where(eq(articles.id, id))
.then((r) => r[0] ?? null);
if (!checkArticleVoter(_req, "DELETE", resource, userId)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
await db.delete(articles).where(eq(articles.id, id));
return NextResponse.json({ success: true });
}