@kizaki/sdk
Server-side query builders, context, mutations, file storage, email, payments, and error types.
@kizaki/sdk is the server-side application surface. It provides query builders, request context, file storage, email, payments, and managed integrations for use inside @expose functions.
Query Builders
Build queries and execute them with query().
import { query, select, insert, update, del, eq } from "@kizaki/sdk";
import { Project } from "@kizaki/schema";
// Read
const projects = await query(
select(Project)
.fields(Project.id, Project.name, Project.createdAt)
.where(eq(Project.ownerId, userId))
.orderBy(Project.createdAt, "desc")
.limit(20)
.offset(0)
);
// Write
await query(insert(Project).values({ name: "Documentation Site", ownerId: userId }));
await query(update(Project).set({ name: "Docs Portal" }).where(eq(Project.id, projectId)));
await query(del(Project).where(eq(Project.id, projectId)));Select Methods
| Method | Purpose |
|---|---|
.fields(...) | Select specific columns |
.where(...) | Filter rows |
.orderBy(field, direction?) | Sort results |
.limit(n) | Cap result count |
.offset(n) | Skip rows (pagination) |
.include(relation) | Eager-load a relation |
.distinct() | Deduplicate rows |
.returning(...) | Return columns after insert/update/delete |
Advanced Builders
- CTEs —
withCTE(name, builder)andwithRecursive(name, base, recursive) - Set operations —
union(a, b),intersect(a, b),except(a, b) - Case expressions —
caseWhen(condition, then).when(condition, then).else(fallback) - Window functions —
rowNumber(),rank(),denseRank(),lag(field),lead(field)with.over() - Aggregation —
aggregate(Entity).groupBy(field).having(filter) - Series —
generateSeries(start, stop, step) - Batching —
batch(queries)runs multiple queries in one round trip;batchSettled(queries)returns results for all queries even if some fail
Filters
Filter functions compare a field to a value. Use inside .where().
import { eq, neq, gt, gte, lt, lte, like, ilike, inArray, isNull, and, or, not } from "@kizaki/sdk";
// Single condition
select(Order).where(eq(Order.status, "shipped"))
// Multiple arguments to .where() are ANDed
select(Order).where(gt(Order.total, 100), eq(Order.status, "paid"))
// Compound filters
select(Order).where(
or(
eq(Order.status, "shipped"),
and(eq(Order.status, "paid"), gt(Order.total, 500))
)
)
// Negation
select(Order).where(not(eq(Order.status, "cancelled")))Filter Reference
| Function | SQL |
|---|---|
eq(field, value) | = |
neq(field, value) | != |
gt(field, value) | > |
gte(field, value) | >= |
lt(field, value) | < |
lte(field, value) | <= |
like(field, pattern) | LIKE |
ilike(field, pattern) | ILIKE (case-insensitive) |
inArray(field, values) | IN |
notInArray(field, values) | NOT IN |
isNull(field) | IS NULL |
isNotNull(field) | IS NOT NULL |
Compound combinators: and(...), or(...), not(filter).
Context
Every @expose function runs with implicit request context via AsyncLocalStorage.
import { getPrincipal, getContext, getLogger, getWideEvent, getTraceId, query } from "@kizaki/sdk";
/** @expose */
export async function dashboardData() {
const principal = getPrincipal(); // { id, email, name, roles }
const ctx = getContext(); // lower-level access to the SDK client
const logger = getLogger(); // structured logger
const wideEvent = getWideEvent(); // per-request wide event enrichment
const traceId = getTraceId(); // W3C trace ID (32-char hex) or null
logger.info("Loading dashboard", { userId: principal.id });
wideEvent.set("view", "dashboard");
return query(select(Project).where(eq(Project.ownerId, principal.id)));
}query() is the top-level query executor. It uses the current principal for policy enforcement. Prefer query() over ctx.sdk.query().
Transactions
Wrap multiple operations in a transaction for atomicity.
import { transaction, insert, update, eq } from "@kizaki/sdk";
import { Order, OrderItem } from "@kizaki/schema";
/** @expose */
export async function placeOrder(items: Array<{ productId: string; quantity: number }>) {
return transaction(async (tx) => {
const order = await tx.query(
insert(Order).values({ status: "pending" }).returning(Order.id)
);
for (const item of items) {
await tx.query(
insert(OrderItem).values({ orderId: order.id, ...item })
);
}
return order;
});
}If any operation throws, the entire transaction rolls back. Transactions are server-only.
File Storage
Files follow a three-step handshake: upload, confirm, retrieve.
import { uploadFile, confirmFile, getFileUrl } from "@kizaki/sdk";
/** @expose */
export async function requestDocumentUpload(filename: string, mimeType: string, size: number) {
// 1. Request upload — returns a token and a presigned upload URL
const { fileToken, uploadUrl, headers } = await uploadFile(
"Document", // entity name
"attachment", // file field name
filename, // original filename
mimeType, // MIME type
size, // file size in bytes
);
return { fileToken, uploadUrl, headers };
}
/** @expose */
export async function confirmDocumentUpload(fileToken: string, entityId: string) {
// 2. Confirm — associates the uploaded file with a row
const result = await confirmFile(fileToken, entityId);
return result; // { sizeBytes, mimeType }
}
/** @expose */
export async function getDocumentUrl(entityId: string) {
// 3. Retrieve — generate a signed download URL
const { downloadUrl, expiresInSeconds } = await getFileUrl(
"Document", // entity name
"attachment", // file field name
entityId, // row ID
);
return { downloadUrl, expiresInSeconds };
}File validation annotations (@maxSize, @accept) declared on file fields in the schema are enforced at upload time.
Send transactional email from @expose functions. Requires an email {} block in your schema. In dev mode, emails are intercepted into the dashboard inbox.
import { sendEmail } from "@kizaki/sdk";
/** @expose */
export async function inviteMember(email: string, projectName: string) {
await sendEmail({
to: email,
subject: `You have been invited to ${projectName}`,
html: `<p>Click <a href="...">here</a> to join.</p>`,
text: `You have been invited to ${projectName}. Visit ... to join.`,
});
}Calendar invites via .ics attachments:
import { createICSInvite, sendEmail } from "@kizaki/sdk";
const invite = createICSInvite({
title: "Project kickoff",
startsAt: "2026-06-03T18:00:00Z",
endsAt: "2026-06-03T18:30:00Z",
});
await sendEmail({
to: "guest@example.com",
subject: "Project kickoff",
text: "Invite attached.",
attachments: [
{
filename: "kickoff.ics",
content: invite,
contentType: "text/calendar",
},
],
});The same email {} config is also used by auth mail such as password reset flows. See Email for the full BYOK/dev-interception story.
Integrations
Use integration helpers when your app declares managed integrations in Inspire.
import {
getIntegrationStatus,
getIntegrationAccessToken,
withIntegrationAccess,
} from "@kizaki/sdk";
/** @expose */
export async function notionWorkspaceName() {
const status = await getIntegrationStatus("notion");
if (!status.connected) {
throw new Error("notion_not_connected");
}
return withIntegrationAccess("notion", async (grant) => {
const response = await fetch("https://api.notion.com/v1/users/me", {
headers: {
authorization: `Bearer ${grant.accessToken}`,
"Notion-Version": "2022-06-28",
},
});
return response.json();
});
}Constraints:
- Server-only
- Operates on the current authenticated user
- Returns short-lived access tokens only
- Refresh tokens never leave the platform control plane
Google Calendar
A first-party typed helper for Google Calendar:
import { googleCalendar } from "@kizaki/google";
/** @expose */
export async function upcomingBusyTime() {
return googleCalendar("calendar").getFreeBusy({
timeMin: "2026-06-01T00:00:00Z",
timeMax: "2026-06-08T00:00:00Z",
calendarIds: ["primary"],
timeZone: "America/New_York",
});
}Available operations:
googleCalendar(key).getStatus()googleCalendar(key).listCalendars()googleCalendar(key).getFreeBusy(...)googleCalendar(key).listEvents(...)googleCalendar(key).createEvent(...)googleCalendar(key).updateEvent(...)googleCalendar(key).cancelEvent(...)
Read operations require access: read or access: write. Mutating operations require access: write. Provider errors surface as typed GoogleCalendarError. Server-only — refresh tokens are never exposed.
Payments
Payment helpers wrap the configured Stripe integration. All three require AsyncLocalStorage context.
import { createCheckout, getSubscription, createPortalSession } from "@kizaki/sdk";
/** @expose */
export async function startCheckout(plan: string) {
const { checkoutUrl } = await createCheckout(plan, { interval: "monthly" });
return { checkoutUrl }; // redirect the user here
}
/** @expose */
export async function billingStatus() {
return getSubscription(); // { plan, status, trialEnd, graceEnd }
}
/** @expose */
export async function manageBilling() {
const { portalUrl } = await createPortalSession("/settings/billing");
return { portalUrl };
}In local dev, use the dashboard's mock payment controls instead of connecting to Stripe.
HTTP Response Helpers
For route handlers (not @expose RPC functions), response helpers set the correct status code and headers.
import { ok, created, noContent, redirect, badRequest, unauthorized, forbidden, notFound, httpError } from "@kizaki/sdk";
return ok({ project }); // 200
return created({ id }); // 201
return noContent(); // 204
return redirect("/dashboard"); // 302
return badRequest("Missing name"); // 400
return unauthorized(); // 401
return forbidden(); // 403
return notFound(); // 404
return httpError(429, "Rate limit exceeded"); // custom statusError Types
Typed error classes for common failure modes.
| Error | When it occurs |
|---|---|
KizakiError | Base class for all SDK errors |
PolicyDeniedError | Current principal lacks permission |
EntityNotFoundError | Single-record query returned no results |
ValidationError | Input failed schema or constraint validation |
RateLimitError | Request was throttled |
TransactionTimeoutError | Transaction exceeded time limit |
TriggerFailedError | Trigger function threw during execution |
DatabaseError | Underlying database error (constraint violation, connection failure) |
All errors expose .toUserMessage() for a human-readable string. Prefer it over raw .message in user-facing code.
Related guide: Write Server Functions