The eight primitives
Everything in the framework is one of these. x g resource <name> scaffolds a whole feature slice — an entity + repo, a policy, an errors module, two actions (create-*, archive-*), a live query, a job, an app route, plus service, UI and i18n files, 25 in all — each with a test that passes on the first run. Not one of each: the slice emits no mutator and no task. Those have their own generators.
entity — a table + its domain type + invariants
policy — an authz rule, evaluated in every surface
action — a mutation or command (server-authoritative)
mutator — an action with an optimistic local twin (offline/realtime)
query — a read; optionally live (subscribable)
job — durable background work, optionally multi-step
route — a URL + render mode + metadata + offline strategy
task — a scheduled trigger (cron) that enqueues jobs| Primitive | Declared in | Generator | Deep page |
|---|---|---|---|
entity | <feature>/entity.ts | x g entity <name> | Entities and migrations |
policy | <feature>/policy.ts | x g policy <name> | Policies and authz |
action | api/ or <feature>/actions/<name>.ts | x g action <name> | Actions |
mutator | <feature>/actions/<name>.ts | x g mutator <name> | Realtime |
query | <feature>/queries/<name>.ts, or live/<name>.ts with --live | x g query <name> --feature <slice> | Queries and live queries |
job | <feature>/jobs/<name>.ts | x g job <name> | Jobs and workflows |
route | a route folder's config export | x g route <path> | Routes and render modes |
task | <feature>/tasks/<name>.ts, plus the job it enqueues | x g task <name> | Scheduled tasks |
Every one is a function returning a value, so an app encodes a house rule by wrapping one and exporting its own factory — tenantEntity, auditedMutator — with the registry, the manifest, the projections, admin and MCP treating the result identically. That is how the framework extends itself too (llm() returns an action, backfill() returns a job), and it is why there is no plugin API → Building your own base.
Every primitive projects itself
A primitive's surfaces are methods on the primitive, never free functions taking it. publishPost.openapi(), not toOpenApiOperation(publishPost). (The MCP tool is @ultimat3/mcp's one projection, toolFrom(publishPost): a tier-3 primitive cannot hold a tier-4 projection.) Every declared field is lifted onto it, and the declaration object — the handle, the sql, the server — is not reachable from app code at all.
| Primitive | Its surface |
|---|---|
entity | $-sigil members: posts.$view([...]), posts.$parse(row), posts.$assert(row). An entity is its columns, so the sigil keeps the namespace clear — posts.name is a column, posts.$name is the entity |
policy | one signature, ({ actor, input, row, ctx }), identical on every surface. row is required and nullable, never smuggled through input |
action | .as() .openapi() .client() .job() .contract() .describe(), plus .input .output .policy .mcp. handle is not among them |
mutator | everything action has, plus .local() .server() .conflict .describeMutator() |
query | .as() .live() .client() .describe(), plus .input .policy .cache .mcp .isLive. sql is not among them |
job | .enqueue() .as() .describe(), plus .parse() and .idempotencyKeyFor() |
route | a normalized descriptor rather than methods — meta() always awaits, budget is always an object. A route declares no behaviour to project; describePages() is the one route list |
task | .entries() .enqueue() .describe() |
The declaration lives in a private store inside the package that runs it, and nothing exports a reader for it. So there is one execution path and one authz path structurally, not by convention: a hand-rolled look-alike carrying the right kind is X_ACTION_FOREIGN / X_QUERY_FOREIGN, never a registered primitive. Registration stamps the export name in place, so import { publishPost } is the object that projects once the app has booted — there is no second, differently-named twin to remember.
Each primitive's deep page carries the full member table.
One import per authoring file
t is re-exported from every package that hosts a declaration factory. Import it from the package you declare in; never reach past that package to @ultimat3/schema. It is the same object, not a copy — configureSchemaProvider() still swaps the provider under it — and each package pins that with an identity assertion in its index.test.ts.
Import t from | When the file declares |
|---|---|
@ultimat3/entity | an entity |
@ultimat3/action | an action or a mutator |
@ultimat3/query | a query |
@ultimat3/jobs | a job or a task |
@ultimat3/mail | a defineMail template |
@ultimat3/mcp | a defineAppMcp hand-written tool |
@ultimat3/ai | an llm — a factory over action, so it carries t too |
@ultimat3/schema | nothing — a plain view-schema module, where schema is the one import |
policy and route declare no schema, so neither package re-exports t: a policy decides from ({ actor, input, row, ctx }) and a route from its path.
entity
A table + its domain type + invariants. The single source of the DB schema, the TS type, and the parse boundary.
| Aspect | Rule |
|---|---|
| Projects to | Postgres table, domain type, migration, repo (postgresRepo), admin screen, seed factory |
| Owns | column types, defaults, invariants, tenant column |
| Never | business logic, I/O, HTTP awareness, policy decisions |
policy
An authz rule, evaluated in every surface.
policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId))| Aspect | Rule |
|---|---|
| Projects to | HTTP guard, live-query row filter, job actor check, MCP tool gate, admin visibility |
| Owns | the yes/no and the denial reason |
| Never | mutate, query outside the declared repos, or return partial data (filter in the query) |
action
A mutation or command, server-authoritative. Declared once, projected six ways.
export const publishPost = action({
input: t.object({ postId: t.uuid, notify: t.boolean.default(true) }),
output: PostView,
policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId)),
cache: { invalidates: [tag.post, tag.feed] },
mcp: { expose: true, description: 'Publish a draft post' },
async handle({ input, ctx }) {
const post = await ctx.posts.publish(input.postId);
if (input.notify) await notifySubscribers.enqueue({ postId: post.id, orgId: post.orgId });
return post;
},
});| Aspect | Rule |
|---|---|
| Projects to | HTTP route, OpenAPI operation, typed client function, job handle, MCP tool, contract test |
| Owns | input/output contract, its policy, what it invalidates, MCP exposure |
| Never | read headers or cookies, render, authorize inside handle, do slow work inline |
mutator
An action with an optimistic local twin. local runs client-side against the local store; server is authoritative. Same input, same name, both halves in one file.
export const likePost = mutator({
// REQUIRED (`X_MUTATOR_NOT_IDEMPOTENT`): a replay under the same Idempotency-Key answers the
// first result instead of running `server` again.
idempotent: true,
// Convergent, not incremental: `local` replays on every rebase, so applying it N times has to
// equal applying it once — `likedByMe` is what makes the second application a no-op.
local(tx, { postId }) {
tx.posts.update(postId, (p) =>
p.likedByMe ? {} : { likedByMe: true, likeCount: p.likeCount + 1 });
},
async server(ctx, { postId }) { return ctx.posts.like(postId); },
conflict: 'server-wins', // | 'last-write-wins' | custom(merge)
});| Aspect | Rule |
|---|---|
| Projects to | everything action does, plus a local-store transaction and a rebase entry |
| Owns | conflict strategy |
| Never | let local do I/O, randomness, or Date.now() — it must be replayable |
query
A read; optionally live (subscribable).
export const liveFeed = query({
input: t.object({ orgId: t.uuid }),
policy: can('feed:read'),
live: true,
sql: ({ orgId }) => db.posts.where({ orgId }).orderBy('createdAt').limit(50),
});| Aspect | Rule |
|---|---|
| Projects to | HTTP GET, typed client hook, live subscription (live: true), cache entry with tags, MCP read tool |
| Owns | shape + row-level filtering |
| Never | write, enqueue, or send mail. live: true requires a deterministic, bounded sql |
job
Durable background work, optionally multi-step. idempotencyKey is required by the type.
export const onboardOrg = job({
input: t.object({ orgId: t.uuid }),
tenant: ({ orgId }) => orgId, // the org this run acts as
idempotencyKey: ({ orgId }) => `onboard:${orgId}`, // REQUIRED by the type
retry: { attempts: 5, backoff: 'exponential' },
async run({ input, step, ctx }) {
const org = await step.run('provision', () => ctx.orgs.provision(input.orgId));
await step.run('welcome-email', () => ctx.mail.send(welcomeEmail, org));
await step.sleep('3d');
await step.run('nudge', () => ctx.mail.send(nudgeEmail, org));
},
});| Aspect | Rule |
|---|---|
| Projects to | queue row, per-step persistence, retry schedule, dashboard entry, MCP jobs.inspect tool |
| Owns | retries, steps, concurrency class |
| Never | assume it runs once — assume at-least-once. Durable business state lives in your tables, never only in the payload |
| Factories over it | backfill(), rollup(), refreshView() (Rollups), consumer() (a job plus its .supervisor task — Consumers), purge(), exportRows(), importRows(), notifier(), webhook() (outbound) and webhookReceiver() (inbound: a job plus the plain route that verifies, enqueues, then acks) — Jobs and workflows |
route
A URL + render mode + metadata + offline strategy.
export const config = defineRoute({
render: 'isr', // static | isr | ssr | stream
revalidate: { tags: [tag.post] },
prerender: () => db.posts.slugs(),
offline: 'precache', // precache | runtime | network-only
hydrate: 'visible', // idle | visible | interaction | never
budget: { js: '40kb' },
meta: ({ post }) => ({ title: post.title, description: post.excerpt,
og: { image: post.cover }, ld: ld.Article(post) }),
});| Aspect | Rule |
|---|---|
| Projects to | router entry, prerender list, sw.js precache/runtime rule, sitemap + RSS row, <head> + JSON-LD, per-route budget check |
| Owns | render mode, hydration timing, metadata, offline strategy |
| Never | touch the DB directly, hold business logic, or omit meta.description in site/ — that is a build error, X_SEO_META_MISSING — the same code for a missing title, with cause naming the field |
Render modes: static (built once), isr (static + background regen), ssr (per-request), stream (shell flushed instantly, holes streamed — default for app pages). Four, declared once in @ultimat3/core as RENDER_MODES; spa was the fifth until 6.0.0. Table in Routes and render modes.
task
A scheduled trigger. Enqueues jobs; never does work itself.
export const nightlyDigest = task({
cron: '0 3 * * *',
tz: 'UTC',
enqueue: () => [[sendDigest, {}]],
});| Aspect | Rule |
|---|---|
| Projects to | scheduler entry (lease-row leader, x_scheduler_leader), next-run introspection, a tasks row in x.manifest.json |
| Owns | cron expression + explicit tz |
| Never | contain a handler body. If it does work, it is a job |
Composition
entity ──> policy ──> action ──> job ──> task
│ │ │
│ └──> query (live) ──> route
└──> mutator (action + local twin)Feature slicing puts a feature's primitives in one folder, not one layer per app:
apps/web/app/<feature>/{entity,repo,service,actions,live,jobs,policy,ui}.tsEvery primitive emits its test beside it, and that test passes on the first run — a generator that emits a TODO has moved the work, not done it. What it pins is the distant invariant the primitive owns: a policy denial, an idempotency key, a budget. Extend it as the feature grows (Testing). Every primitive appears in x.manifest.json, so x manifest --json and the MCP manifest.read tool describe the whole app as data.
The rule
If a feature doesn't fit a primitive, it doesn't ship. No ninth primitive, no escape-hatch mode: option, no plugin API in 1.0. Removing an alternative is a feature — ambiguity is the tax agents pay. Source: docs/idea/02-primitives.md.