Kizaki
ReferenceTypeScript

@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

MethodPurpose
.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

  • CTEswithCTE(name, builder) and withRecursive(name, base, recursive)
  • Set operationsunion(a, b), intersect(a, b), except(a, b)
  • Case expressionscaseWhen(condition, then).when(condition, then).else(fallback)
  • Window functionsrowNumber(), rank(), denseRank(), lag(field), lead(field) with .over()
  • Aggregationaggregate(Entity).groupBy(field).having(filter)
  • SeriesgenerateSeries(start, stop, step)
  • Batchingbatch(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

FunctionSQL
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.

Email

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 status

Error Types

Typed error classes for common failure modes.

ErrorWhen it occurs
KizakiErrorBase class for all SDK errors
PolicyDeniedErrorCurrent principal lacks permission
EntityNotFoundErrorSingle-record query returned no results
ValidationErrorInput failed schema or constraint validation
RateLimitErrorRequest was throttled
TransactionTimeoutErrorTransaction exceeded time limit
TriggerFailedErrorTrigger function threw during execution
DatabaseErrorUnderlying 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

On this page