Sub-skills
Workspaces in depth
For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill workspaces`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/workspaces.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Several apps of one organization in one directory: typed calls (RPC), durable events, a shared database. One app is simpler: split only when parts deploy, scale or are owned separately.
cubek init --workspace shop --apps web,billing # empty apps in apps/web, apps/billing
cubek dev --workspace # every app, one port each
cubek check --workspace # every app, in dependency order
cubek deploy --workspace --prod # callees first
cubek dev --stop # in the workspace directory: stops all// cubek-workspace.json
{ "name": "shop", "apps": ["apps/web", "apps/billing"] }Each app is a normal project; its name is name in its cubek.json.
Calls (RPC)
The callee exports plain async functions from src/rpc.ts and lists its
callers; arguments and results are plain data.
// apps/billing/cubek.json
{
"name": "billing",
"entry": "src/app.ts",
"schema": "schema.ts",
"rpc": { "allow": ["web"] },
"events": { "subscribe": ["order.paid"] }
}// apps/billing/schema.ts
import { table, t } from "cubek"
export const invoices = table("invoices", { id: t.id(), orderId: t.string().unique(), total: t.number(), createdAt: t.timestamp().default("now") }).index("createdAt")// apps/billing/src/rpc.ts
import { db, rpcCaller } from "cubek"
export async function createInvoice(input: { orderId: string; total: number }): Promise<{ invoiceId: string }> {
if (input.total < 0) throw new Error("negative total")
// Idempotent: a retried call gets the same invoice.
const seen = await db.invoices.findOne({ where: { orderId: input.orderId } })
if (seen !== null) return { invoiceId: seen.id }
const who = rpcCaller() // { app, org } of the caller
if (who === null) throw new Error("rpc only")
const inv = await db.invoices.insert({ orderId: input.orderId, total: input.total })
return { invoiceId: inv.id }
}
export const rpc = { createInvoice }// apps/billing/src/app.ts
import { app, db, events, log } from "cubek"
app.get("/api/invoices", async (c) => c.json(await db.invoices.find({ orderBy: { createdAt: "desc" }, limit: 50 })))
// An event handler is a task: at least once, retried; make it idempotent.
events.on("order.paid", async (p: { orderId: string; total: number }) => {
log.info("order paid", { orderId: p.orderId })
})// apps/web/cubek.json
{ "name": "web", "entry": "src/app.tsx", "schema": "schema.ts" }// apps/web/schema.ts
import { table, t } from "cubek"
export const orders = table("orders", { id: t.id(), total: t.number(), invoiceId: t.string().nullable() })// apps/web/src/app.tsx
import { app, apps, db, events } from "cubek"
app.post("/orders", async (c) => {
const order = await db.orders.insert({ total: 42 })
try {
const inv = await apps.billing.createInvoice({ orderId: order.id, total: order.total })
await db.orders.update(order.id, { invoiceId: inv.invoiceId })
} catch (e) {
// "RPC_THROWN: billing.createInvoice: negative total", RPC_DENIED, RPC_UNAVAILABLE...
return c.json({ error: e instanceof Error ? e.message : "rpc failed" }, 502)
}
await events.publish("order.paid", { orderId: order.id, total: order.total })
return c.json(order, 201)
})cubek check(orcheck --workspace) writes the callee's types into the caller's.cubek/apps/: calls are type-checked. A dashed name isapps["photo-media"].fn(...).- Errors arrive as
Errorwhose message starts with a code:RPC_THROWN,RPC_LIMIT,RPC_DENIED(not inrpc.allow),RPC_NOT_FOUND,RPC_UNAVAILABLE,RPC_TOO_MANY,RPC_BAD_VALUE. - Limits: depth 8, 50 calls per execution, 8 MB per call.
- Events:
events.publish(name, payload)is durable; every app listing the name inevents.subscribegets one delivery (at least once, 8 retries, then a dead letter). Each subscription needs itsevents.on, and eachevents.onits subscription (check says so).
Shared database
An app can use another app's database instead of its own: cubek.json
"bindings": { "db": "web" } and no schema.ts. db is typed from the
owner's schema; only the owner migrates. Use it for an admin app over the
main app's data.
Deployed behavior
- Calls, events and bound databases reach the other app's production
revision, also from a preview: deploy callees with
--prodfirst (deploy --workspace --prodorders them). cubek dev --workspaceserves each app on its own port and wires calls and events locally. Apps may alsofetcheach other's public URLs (in dev, their local URLs).