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 (or check --workspace) writes the callee's types into the caller's .cubek/apps/: calls are type-checked. A dashed name is apps["photo-media"].fn(...).
  • Errors arrive as Error whose message starts with a code: RPC_THROWN, RPC_LIMIT, RPC_DENIED (not in rpc.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 in events.subscribe gets one delivery (at least once, 8 retries, then a dead letter). Each subscription needs its events.on, and each events.on its 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 --prod first (deploy --workspace --prod orders them).
  • cubek dev --workspace serves each app on its own port and wires calls and events locally. Apps may also fetch each other's public URLs (in dev, their local URLs).