Client

Overview

Typed caller for your Flows — createClient from okengine/client, zero codegen, errors as values.

okengine/client is how a browser, CLI, or another service calls your app's Flows. Import the generated barrel, take typeof app, and api.bookings.get({ id }) is fully typed — same contracts the server already has, no separate schema project.

For developers consuming an okengine app from the outside — create the client, call a Flow, narrow the envelope.

The one rule

Treat every call as a result envelope: { data, error }. Flow failures are values you switch on (error.code); they are never thrown. Only transport / protocol problems use code: "TransportError".

Smallest Example

Export App from the server

src/app.ts
import "@/core";
import "@/flows/generated";
import { oke } from "okengine/http";

export const app = oke({ name: "commerce" });
export type App = typeof app;

.adopt({ bookings }) is optional and additive when a unit is not already in the generated barrel.

Load a booking from the client

web/src/api.ts
import { createClient } from "okengine/client";
import { vault } from "okengine/vault";
import { app } from "../../src/app";

// Base URL via vault.env (empty = same-origin proxy in create-oke web)
const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "");
const { data, error } = await api.bookings.get({ id: "bkg_7f3a" });

if (error) {
  // TransportError or a declared flow code (NotFound, …)
  return;
}

console.log(data.confirmationCode, data.seats);

With $routes wired that is GET /bookings/bkg_7f3a on the backend (port 6530):

{
  "data": {
    "id": "bkg_7f3a",
    "confirmationCode": "SK-4812",
    "seats": 2,
    "status": "confirmed"
  },
  "error": null
}

Progressive Patterns

Same typed proxy from same-repo REST to a separate frontend repo:

Pass the app value so HTTP triggers hit REST (method + path from $routes):

import { createClient } from "okengine/client";
import { vault } from "okengine/vault";
import { app } from "./app";

const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "");
await api.bookings.get({ id: "bkg_7f3a" }); // GET /bookings/bkg_7f3a

From App to client

one App type
  1. 01

    App + routes

    app.$routes · oke-client.routes

    Typed adopt or generated routes module — contracts for every Flow.

  2. same Manifest
    02

    createClient

    createClient(url, { $routes, auth? })

    Optional auth attaches api.auth. Same App type in browser or SSR.

  3. same Manifest
    03

    Envelope + ambient

    { data, error } · oke-client.d.ts

    Every call returns data and error. Ambient types after oke client add.

Adopt remains available for same-repo typing — generated routes are the handbook path.

Flows only

Flows only

The client calls Flows. Every other element runs on the server through fx. You reach its outcome by calling a Flow that uses it — or by handling a gate denial on that call.

ElementOn the clientHow
FlowDirectapi.unit.flow(input) — the only public surface
GateIndirectBearer via auth; denials as Unauthorized / Forbidden / RateLimited
StoreVia Flowsfx.store inside Flows; store.resource + on(http.resource…) → five Flows on $routes
SignalLive SSEapi.live(signal, input?, { onEvent }) — HTTP GET, callback + unsubscribe
ClockVia FlowsSchedules fire on the server — the client never ticks a clock
VaultVia FlowsSecrets stay server-side; never ship them to the browser package
ChannelVia Flowsfx.send in a Flow — the client does not send email/SMS/push
AIVia Flowsfx.ask / fx.run inside a Flow; the client gets that Flow’s out

Pages

Learn more

  • Routing — folders are the URL; $routes without .adopt()
  • HTTP — verbs, http.resource, live SSE
  • The Architecture — generated barrel → client → test loop; same contract for Client, Console, MCP
  • Errors — framework codes vs failure values

Next

On this page