Server integration
Read and write a workspace's private data from your own server — internal tools, sync jobs, provisioning.
Use this path when your code runs on a server you control and acts as the workspace: an
internal dashboard over contacts and deals, a nightly sync of employees into payroll, a script
that provisions a new site. It needs an account Bearer token (apiKey), never shipped to a
browser.
By the end you'll have a server client that reads private data, walks whole collections, writes with proper error handling, and the CLI for the same work from a terminal.
Create a server client with apiKey
One client, built with the account Bearer token and imported only from server code: a route handler, a server action, a Server Component or a plain Node script.
// lib/uidu-admin.ts — never imported from a 'use client' file
import { createClient } from '@uidu/client';
export const uiduAdmin = createClient({
workspace: process.env.UIDU_WORKSPACE!,
apiKey: process.env.UIDU_API_KEY, // server-only — no NEXT_PUBLIC_ prefix
});Where the token comes from, and why it never reaches the browser: Authentication.
Read private data
Contacts and deals, people and goals are readable only with the Bearer. A route handler that feeds an internal dashboard:
// app/api/dashboard/route.ts
import {
listContacts,
listDeals,
listEmployees,
listGoals,
goalProgress,
isGoalBehind,
} from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
export async function GET() {
const [contacts, deals, employees, goals] = await Promise.all([
listContacts(uiduAdmin), // every contact — not paginated
listDeals(uiduAdmin, { first: 50 }),
listEmployees(uiduAdmin, { first: 50 }),
listGoals(uiduAdmin),
]);
const behind = goals
.filter((g) => !g.parentId) // objectives, not key results
.filter((g) => isGoalBehind(g, { tolerance: 0.1 }))
.map((g) => ({ name: g.name, done: goalProgress(g), status: g.status }));
return Response.json({
contacts: contacts.length,
openDeals: deals.map((d) => ({ name: d.name, amount: d.amount, status: d.status?.name })),
people: employees.map((e) => e.user?.name),
behind,
});
}Empty is a symptom
A list that comes back empty (or a get* that returns null) usually means the client was built
with a public token: private data isn't an error for it, just invisible. Check that the client
has apiKey before debugging the query.
Fields, filters and helpers: Contacts & deals (amount is in cents),
People, Goals (values stored ×100).
Read everything
list* returns one page. For a sync job or an export, paginate walks every page of a list
with its generated *Document:
import { paginate, ListDealsDocument, ListEmployeesDocument } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
const deals = await paginate(uiduAdmin, ListDealsDocument, {
variables: { first: 100 },
extract: (data) => data.currentWorkspace?.deals,
});
const employees = await paginate(uiduAdmin, ListEmployeesDocument, {
variables: { first: 100 },
extract: (data) => data.currentWorkspace?.employees,
});listContacts already returns every contact: the field isn't a connection, so there is nothing
to paginate. More in Pagination & field values.
Write, and handle errors
Every write takes { input: { attributes } } (plus id to update) and resolves to a payload
with an errors array. A non-empty errors is a validation failure — nothing is thrown, and
the record is unchanged. A transport or GraphQL failure, instead, throws a UiduError.
'use server';
import { updateDeal, UiduError } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
export async function moveDeal(id: string, statusId: string) {
try {
const result = await updateDeal(uiduAdmin, {
input: { id, attributes: { statusId } },
});
if (result?.errors?.length) {
// validation: e.g. [{ key: 'status', name: '…', type: '…' }]
return { ok: false, errors: result.errors.map((e) => e.name) };
}
return { ok: true, id: result?.deal?.id };
} catch (err) {
if (err instanceof UiduError && err.code === 'GRAPHQL_ERROR') {
console.error(err.graphQLErrors);
return { ok: false, errors: ['The server rejected the request.'] };
}
throw err; // NETWORK_ERROR and anything else: let it surface
}
}Where the record sits in the payload varies by mutation — updateDeal returns it at deal,
createDeal at deal.node, createTask at task. The domain page shows each one. The input
shape and the full capability matrix: Writes; error codes and what to
retry: Errors.
Provision from the terminal
The uidu CLI runs the same functions with the same Bearer: sign in once, then
<entity> list|get|create|update|delete. With --json, stdout carries only JSON, so you can pipe
it.
uidu login --workspace acme # browser sign-in, token stored in ~/.uidu/config.json
uidu whoami --json # check the workspace and that the token hasn't expired
# read
uidu deals list --first 50 --json
uidu employees list --json | jq -r '.[].user.email'
# write — exits non-zero and prints the errors when `errors` isn't empty
uidu tasks create --attributes '{"name":"Call back Acme","spaceId":"Z2lkOi8v…"}' --json
uidu deals update Z2lkOi8v… --attributes '{"statusId":"Z2lkOi8v…"}' --jsonIn CI, set UIDU_WORKSPACE and UIDU_API_KEY instead of running login (flags win over env,
env over ~/.uidu/config.json). Every verb per entity: CLI commands;
sign-in flows and token expiry: Login.
Run actions the SDK doesn't cover yet
The platform exposes more actions than the SDK has typed functions for. Any of them runs today,
by key, through the generic dispatcher — executeAction in code, uidu tools call in the
terminal. attributes uses the entity's own camelCase fields, and associations are GlobalIDs (the
id a previous list or get returned).
import { executeAction } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
const result = await executeAction(uiduAdmin, {
input: {
key: 'Budgets::Budget::Create',
attributes: { fiscalYear: 2027, currency: 'EUR' },
},
});
if (result?.errors?.length) {
// same rule as every write: validation errors come back, they aren't thrown
} else {
result?.result; // what the action returned, as JSON
}
// The same from the terminal:
// uidu tools call Budgets::Budget::Create --attributes '{"fiscalYear":2027,"currency":"EUR"}' --jsonFor an action on an existing record, pass its id in input as well — executeAction only;
uidu tools call takes --attributes alone. Every available key, and which ones the SDK covers:
Roadmap. Field names per entity: Schema.
Where to go next
- Writes — the input shape,
errors, and which verbs each entity supports. - Paginate every page — worked examples of
paginate. - Bookings and Campaigns — the other private-data domains.
- Spaces, tasks & notes —
createTask,createNote,createSpace. - The
uiduCLI — everything the terminal can do, including scaffolding. - Client reference — every function, with its auth.