Skip to content

Consume Tinbox from your own repo

Wire the tinbox package into a sibling repo before npm publish — the file: dep form, build-first, the exports subpaths, the zod-4 peer, and exactly what init/setup write.

You want to use Tinbox from a separate repo — to deploy your own instance, or to author skills/agents against the live engine types — and Tinbox isn't on npm yet. This guide is the shortcut past the archaeology: the dependency form, the build-first rule, the exports subpaths that matter, the peer versions, and a manifest of exactly what init/setup write so you can plan around them.

The dependency, before npm

The in-repo example uses "tinbox": "workspace:*", which only resolves inside this monorepo. From a sibling repo, point the dep at the built package directory:

// your-repo/package.json (or apps/<x>/package.json)
{
  "dependencies": {
    "tinbox": "file:../path/to/tinbox-mono/packages/tinbox"
  }
}

Two things that bite here:

  1. Get the relative depth right. It's relative to the package.json that declares it, not the repo root. From your-repo/apps/tinbox/package.json to a sibling tinbox-mono, that's typically file:../../../tinbox-mono/packages/tinbox — count the .. segments carefully.
  2. Isolate the cross-repo file: dep from your root lockfile. Give the consuming project its own pnpm-workspace.yaml + lockfile so the file: link doesn't leak into your monorepo's root lockfile (this is how the drivechina tenant is structured).

Build the package first — dist/ must exist

The tinbox package ships built — its exports resolve to dist/:

"exports": {
  "./cf":        { "import": "./dist/cf/index.js",  "types": "./dist/cf/index.d.ts" },
  "./cf/skills": { "import": "./dist/cf/skills.js", "types": "./dist/cf/skills.d.ts" },
  "./cf/types":  { "import": "./dist/cf/types.js",  "types": "./dist/cf/types.d.ts" },
  "./cf/plugin": { "import": "./dist/cf/plugin.js", "types": "./dist/cf/plugin.d.ts" },
  "./cf/agents": { "import": "./dist/cf/agents.js", "types": "./dist/cf/agents.d.ts" },
  "./sdk":       { "import": "./dist/sdk.js",       "types": "./dist/sdk.d.ts" }
}

A fresh checkout has no dist/ — import "tinbox/cf/skills" resolves to dist/cf/skills.js, which doesn't exist until a build runs. So:

# in tinbox-mono
pnpm -F tinbox build
# then re-install in your consuming repo so it picks up the fresh dist/
pnpm install --filter your-project

The tinbox build ergonomic. When your instance is file:-linked to a source checkout, tinbox build (a CLI verb) rebuilds the source package and relinks it before you deploy — so a file: consumer ships its latest dist/ without you remembering the two-step. tinbox setup / the tenant redeploy script run it for you.

The subpaths you'll actually import:

ImportWhat it is
tinbox/cfcreateWorker() + TinboxConfig — the deploy entrypoint
tinbox/cf/skillsdefineSkill, builtinSkills, runEvals, stubFetch — author + test skills
tinbox/cf/typesthe worker env / binding types
tinbox/cf/agents · tinbox/cf/pluginagent config + the better-auth plugin
tinbox/sdkthe Resend-style sending SDK

Peer versions — pin zod to 4

The engine and its auth stack (better-auth / better-call) require zod 4 (^4.4.3). tinbox declares zod as a peerDependency with that major pinned, so a consumer on zod 3 gets an actionable warning rather than a silent dual-zod schema-identity bug (two zod copies whose instanceof checks don't match, so inputSchema validation mysteriously fails). If you author skills/agents with zod schemas, install the matching major:

pnpm add zod@^4.4.3

(The peer is marked optional, so a deploy-only consumer that never imports zod isn't forced to add it — but the moment you write a schema, match the major.)

What init / setup write — the output manifest

tinbox init / tinbox setup provision live Cloudflare resources, so you can't run them headless — but you can plan around them. Two ways to see the shape without touching Cloudflare:

  • tinbox setup --dry-run (alias --print-plan) prints the full plan — every resource it would create, the wrangler.jsonc ids it would fill, the secrets it expects, and the phase order — with no auth and no side effects.
  • The manifest below.

Files written / edited:

PathWhat happens
wrangler.jsoncthe REPLACE_WITH_* placeholders for d1_databases[].database_id and kv_namespaces[].id are replaced with the real resource ids (comment-preserving)
tinbox.config.tsauth.owner is set to the sign-in email that may claim first-run ownership: Tinbox-auth by default, or local/external auth if the tenant skips Tinbox-auth
.env.prodgitignored — the local record of generated secrets that were pushed to Cloudflare
.tinbox/logs/<ts>-setup.loga tee of the run's output (gitignored)

Cloudflare resources created (idempotent — existing ones are left as-is):

  • a D1 database (tinbox-<org>-db), an R2 bucket (tinbox-<org>-mail), a KV namespace (tinbox-<org>-kv), and the Queues referenced in wrangler.jsonc (-newsletter, -newsletter-dlq, -projection).

Secrets generated/pushed (to wrangler secret put + mirrored to .env.prod):

  • BETTER_AUTH_SECRET (required), ASSET_SIGNING_KEY (required), the VAPID_* trio (Web Push, generated), and any external key you supply (e.g. ANTHROPIC_API_KEY).

The one inherently-manual step: inbound Email Routing is account-level (not expressible in wrangler.jsonc). setup can enable it + the catch-all → your worker when you give it a CLOUDFLARE_API_TOKEN with email_routing:write; otherwise it's a one-time dashboard step. tinbox doctor verifies it either way.

Half-configured wrangler.jsonc caveat. A scaffolded config carries REPLACE_WITH_* placeholders, so it can't wrangler deploy (or dev) until setup fills the real ids. If you commit it, a teammate/CI that pulls it can't deploy until they run tinbox setup (which is idempotent and fills them). The scaffold sets workers_dev: true so once deployed there's an immediate *.workers.dev URL to smoke-test while a custom domain's cert provisions.

See also

  • Writing a skill — the defineSkill API and how to TDD a skill offline with runEvals.
  • Deploy to Cloudflare — the full deploy paths.
  • tinbox doctor — read-only "what's set up vs still needed", with a --wait that blocks until your custom domain serves.