Routes And API Keys
Expose HTTP endpoints from your app and control non-session access with scoped API keys.
Use routes when something outside the generated Kizaki client needs to call your app over HTTP. That usually means a third-party system, a machine-facing API, or a webhook provider. For your own browser app, exposed functions plus @kizaki/client remain the default.
Start With The Boundary
Before adding an endpoint, ask who is calling it and why it is not an exposed function.
- Generated client for your own app's typed browser-to-server workflows.
- Session routes when you need HTTP semantics for signed-in users.
- API-key routes for integrations and machine callers.
- Public routes only for webhook-style verification flows.
Make that decision early and the rest of the route design falls into place.
A Good Integration Setup
Keep the external API surface small, explicit, and capability-based.
apiKeys {
scopes: {
read: @system("api_read") {
@grant read on Project
@grant read on Task
},
writer: @system("api_writer") {
@grant read on Project
@grant read, write on Task
},
}
}
routes("/v1", auth: apiKey(read), rateLimit: 1000/minute) {
GET /projects -> listProjectsRoute
GET /projects/:projectId -> getProjectRoute
POST /projects/:projectId/tasks -> createTaskRoute (auth: apiKey(writer))
}Routes are grouped by API surface. Read and write capabilities are separate. The route table tells you exactly what the outside world can call.
Implement Thin Handlers
Treat handlers as HTTP adapters, not as a second application architecture.
import { Project, Task } from "@kizaki/schema";
import {
RouteRequest,
badRequest,
created,
insert,
ok,
query,
select,
} from "@kizaki/sdk";
export async function listProjectsRoute(_req: RouteRequest) {
const rows = await query(
select(Project)
.fields(Project.id, Project.name)
.orderBy("createdAt", "desc"),
);
return ok(rows);
}
export async function createTaskRoute(req: RouteRequest) {
const input = req.body<{ title?: string }>();
if (!input?.title) {
return badRequest("title is required");
}
const [task] = await query(
insert(Task)
.values({
projectId: req.params.projectId,
title: input.title,
})
.returning(),
);
return created(task);
}Good handlers do three things: read HTTP input, call normal application logic or entity operations, and return an HTTP-shaped response. If a handler is full of business rules, too much of the app lives behind raw routes.
Choose Auth Mode On Purpose
Session Routes
Use session when the caller is one of your signed-in users.
routes("/app", auth: session) {
GET /me/export -> exportMyDataRoute
}This is useful for HTTP download endpoints, streaming responses, callback URLs, or other browser-facing HTTP shapes that do not fit an exposed function.
API-Key Routes
Use apiKey(scope) when the caller is another system.
routes("/partner", auth: apiKey(read)) {
GET /projects -> listProjectsRoute
}This is the default for integrations, data sync, ingestion, and external automation.
Public Routes
Use none only when the provider gives you another trust mechanism.
routes("/webhooks", auth: none) {
POST /github -> githubWebhookRoute
}Then verify the request inside the handler.
import { RouteRequest, badRequest, ok } from "@kizaki/sdk";
export async function githubWebhookRoute(req: RouteRequest) {
const signature = req.headers["x-hub-signature-256"];
if (!signature) {
return badRequest("missing signature");
}
// Verify req.rawBody with the provider's signing secret here.
return ok({ received: true });
}This pattern is appropriate for webhooks, not for general APIs.
Manage Keys Deliberately
For local operations or team workflows, use the CLI.
kizaki api-keys create --scope read --name "Warehouse sync"
kizaki api-keys create --scope writer --name "CRM importer" --expires 90d
kizaki api-keys list
kizaki api-keys rotate <id> --grace-period 24h
kizaki api-keys revoke <id>For customer-facing product flows, manage keys from your own server functions.
import { getContext } from "@kizaki/sdk";
/** @expose */
export async function createIntegrationKey(name: string) {
const { client } = getContext();
return client.apiKeys.create({
scope: "read",
name,
metadata: { createdFrom: "settings" },
});
}The returned token is shown once. Your UI should treat it like any other secret and never expect to retrieve it again.
Common Mistakes
- Using routes for your whole frontend instead of exposed functions.
- Creating one broad API-key scope for every integration.
- Using
auth: nonefor convenience instead of actual webhook verification. - Putting authorization logic only in handlers instead of the data model.
- Making handlers own business workflows that should live in reusable server code.
A Good Mental Model
Routes are your app's public HTTP shell. API keys define which machine callers exist. Your schema owns the structural access model and your server code owns the workflow logic. When those responsibilities stay separate, routes stay easy to reason about.