API and realtime

Typed oRPC calls from the Vue app, TanStack Query helpers, live streams, and the REST client.

oRPC client

apps/web/src/api/orpc.ts:

import type { TamanContract } from '@taman/api-contract';
import type { ContractClient } from '@vinicunca/request/orpc';
import { createRpcClient } from '@vinicunca/request/orpc';
import { createRpcQueryUtils } from '@vinicunca/request/orpc-query';

export const client = createRpcClient<ContractClient<TamanContract>>({
  url: `${apiUrl.replace(/\/+$/, '')}/api/rpc`,
  credentials: 'include',
});

export const orpc = createRpcQueryUtils(client, { path: ['taman'] });

createRpcClient options (@vinicunca/request/orpc):

url
string required
Full RPC endpoint, e.g. https://api.example.com/api/rpc.
fetch
RpcFetch
Override fetch (tests, SSR, Worker-to-Worker).
headers
object | () => headers
Extra headers per request (object or async function).
credentials
RequestCredentials
No default. Pass 'include' for cross-origin cookie auth.

Re-exported: safe, isDefinedError, ORPCError, LIVE_RETRY.

import { isDefinedError, safe } from '@vinicunca/request/orpc';

const [error, todo] = await safe(client.todo.get({ id }));
if (isDefinedError(error) && error.code === 'NOT_FOUND') {
  // typed contract error
}

TanStack Query

import { useMutation, useQuery } from '@tanstack/vue-query';
import { orpc } from '#/api/orpc';

const list = useQuery(orpc.todo.list.queryOptions({
  input: { page: 1, pageSize: 20 },
}));

const create = useMutation(orpc.todo.create.mutationOptions({
  onSuccess: () => queryClient.invalidateQueries({ queryKey: orpc.todo.list.key() }),
}));

createRpcQueryUtils wraps @orpc/tanstack-query. The path option prefixes every query key (['taman'] here). queryClient is the shared instance in apps/web/src/query-client.ts (also used by the router guard).

Live streams

Contract: todo.live → eventIterator(todoEventSchema). Server: TodoService.subscribe on channel todo:${organizationId}.

Reconnect after drops:

import { LIVE_RETRY } from '@vinicunca/request/orpc';

// pass context: LIVE_RETRY on the live call

LIVE_RETRY sets retry: Infinity and retries unless the error is an ORPCError with status < 500. CORS allows Last-Event-ID. Publisher: Durable Object on Cloudflare (TODO_PUBLISHER), MemoryPublisher in nitro dev (in-process only — multiple Node processes will not see each other's events). Resume retention is 300 seconds.

REST client

Taman's product API is oRPC. @vinicunca/request/http is for other HTTP APIs. The web app's example is dummyjson:

import { createHttpClient } from '@vinicunca/request/http';
import { createHttpQueryUtils } from '@vinicunca/request/http-query';

export const dummyjsonClient = createHttpClient({ baseURL: 'https://dummyjson.com' });
export const dummyjson = createHttpQueryUtils(dummyjsonClient, { key: ['dummyjson'] });

Nothing is sent with cookies unless you pass credentials: 'include'. Install interceptors in this order when you add a real REST backend: envelope → refresh token → error message. See @vinicunca/request.

Contract location

packages/shared/api-contract is the source of truth. Inputs/outputs: TamanInputs, TamanOutputs, TamanContract. Keep the web and API on the same workspace package so types never drift.