# tako.sh SDK

JavaScript/TypeScript SDK for apps running on Tako.

Package name: `tako.sh`

## What It Provides

- Runtime state from `tako` exported by `tako.sh` — typed by your project-local `tako.d.ts`, discoverable, and regenerated by `tako init` / `tako dev` / `tako deploy`
- File-based channel definitions via `defineChannel` in `<app_root>/channels/*.ts` (default-exported handle)
- File-based workflow definitions via `defineWorkflow` in `<app_root>/workflows/<name>.ts` (default-exported handle with `.enqueue`)
- Public optimized image URLs and responsive sources via `imageUrl` and `imageSrcSet` from `tako.sh`
- Typed object storage bindings via `tako.storages`
- Vite plugin for SSR framework builds
- Built-in internal status endpoint (`GET /status` on `Host: <app>.tako`)
- Built-in internal channel auth + dispatch endpoints on `Host: <app>.tako`

The `tako.sh` package's named exports include the `tako` runtime object, definition helpers (`defineChannel`, `defineWorkflow`), the workflow `signal` function, `TakoError`, public image helpers, storage types, and types (`InferChannel`, `InferWorkflowPayload`, `TakoErrorCode`, `EnqueueOptions`, `WorkflowOpts`). There is no `Tako` global — channels and workflows are plain ES modules that you import where you use them.

## Install

```bash
bun add tako.sh
```

## Basic Usage

```ts
import { tako } from "tako.sh";

export default function fetch(req: Request): Response {
  tako.logger.info("request", { path: new URL(req.url).pathname });
  return new Response("ok");
}
```

`tako.d.ts` is generated under the JavaScript `app_root` from `tako.toml` (`src` by default). It augments `tako.sh` with typed secrets, environment names, channel metadata inferred from channel exports, and environment-narrowed `process.env` types. App code imports runtime values from `tako.sh`, not from the generated file.

## Images

Use `imageUrl` to generate public optimizer URLs for local public assets and allowlisted remote images:

```ts
import { imageSrcSet, imageUrl } from "tako.sh";

const photo = imageUrl("/photos/p_123.jpg");

const avatar = imageUrl("/avatars/u_123.png", {
  width: 640,
});

const avatarFallback = imageUrl("/avatars/u_123.png", {
  width: 640,
  format: "webp",
});

const hero = imageUrl("/assets/hero.jpg", {
  width: 1200,
  quality: 80,
});

const responsiveHero = imageSrcSet("/assets/hero.jpg", {
  layout: "constrained",
  width: 1200,
});
```

`imageSrcSet` returns `{ src, srcSet, sizes }` for plain `<img>` usage. Layouts are `constrained` for images that scale down but stop at `width`, `full-width` for viewport-wide images, and `fixed` for fixed-width images. Pass `widths` and `sizes` for custom responsive markup.

Public image URLs use `/_tako/image?src=...&w=...`. Local public paths work by default, and remote URLs must match `[images].remote_patterns` in `tako.toml`.

## Storage

Attach S3-compatible storage with the Tako CLI, then use `tako.storages.<name>` from server-side app code:

```bash
tako storages add uploads \
  --provider s3 \
  --bucket app-uploads \
  --endpoint https://<account>.r2.cloudflarestorage.com \
  --access-key-id <key-id> \
  --secret-access-key <secret> \
  --expires-at "in 90 days"
```

```ts
import { tako } from "tako.sh";

const downloadUrl = await tako.storages.uploads.createDownloadUrl("receipts/r_123.png", {
  expiresInSeconds: 3600,
});

const uploadUrl = await tako.storages.uploads.createUploadUrl("avatars/u_123.png", {
  contentType: "image/png",
});
```

Storage URLs are private by default and signed with SigV4. If the storage was attached with `--public-base-url`, `createImageUrl(..., { public: true, width })` returns a public optimizer URL for that object, and `createImageSrcSet(..., { public: true, layout, width })` returns public responsive image sources.

## Channels

Declare one channel per file in `<app_root>/channels/*.ts`. The default export is the channel handle:

```ts
// <app_root>/channels/chat.ts
import { defineChannel } from "tako.sh";

type ChatMessages = {
  msg: { text: string; userId: string };
  typing: { userId: string };
};

export default defineChannel({
  name: "chat",
  paramsSchema: (t) => t.Object({ roomId: t.String({ minLength: 1 }) }),
  auth: {
    headerName: "authorization",
    async verify(input) {
      const userId = await getUserId(input.header);
      return userId ? { subject: userId } : false;
    },
  },
}).$messageTypes<ChatMessages>();
```

- The `name` property is the wire channel name. New scaffolded files use the file stem as the default name, but Tako does not rewrite existing channel files if you choose a different name.
- Dynamic values are typed query params from `paramsSchema`.
- `auth` is optional — omit or set `false` for public channels. Declarative auth receives `{ header?, cookie?, params, channel, operation }`.
- Call `.$messageTypes<T>()` to type the per-message-type payloads. Presence of a `handler` option (not shown) chooses transport: WebSocket when present, SSE otherwise.
- Browser subscriptions keep reconnecting until closed. The SDK retries through network loss, laptop sleep, server restarts, and clean stream rotation, then resumes from the last received message id inside the bounded replay window.

Publish by importing the channel wherever you need it:

```ts
// server-side code, a workflow, a route handler — anywhere
import chat from "../channels/chat";
import status from "../channels/status";

// Unparameterized channel
await status.publish({ type: "ping", data: { at: Date.now() } });

// Parameterized channel — bind params then publish
const room = chat({ roomId: "room1" });
await room.publish({ type: "msg", data: { text: "hi", userId: "u-1" } });
```

## Workflows

Declare one workflow per file in `<app_root>/workflows/<name>.ts`. The default export exposes `.enqueue(payload)`:

```ts
// <app_root>/workflows/send-email.ts
import { defineWorkflow } from "tako.sh";

export default defineWorkflow<{ userId: string }>("send-email", {
  retries: 4,
  schedule: "0 9 * * *",
  handler: async (payload, ctx) => {
    ctx.logger.info("send-email started");
    await ctx.run("send", (step) => {
      step.logger.info("calling mailer");
      return sendEmail(payload.userId);
    });
  },
});
```

Set `worker: "name"` in the workflow opts to assign a workflow to a named worker group; omitted workflows belong to the `default` group.

Enqueue from anywhere — route handlers, other workflows, scripts:

```ts
import sendEmail from "./workflows/send-email";

await sendEmail.enqueue({ userId: "u1" });
```

To wake a parked `ctx.waitFor`, import `signal` from `tako.sh` and call it with the matching event name:

```ts
import { signal } from "tako.sh";

await signal(`approval:order-${orderId}`, { approved: true });
```

`signal` only works in code that runs under a Tako process (server-side request handlers, workflow workers, scripts run via the SDK's runtime). Browser code throws a `TakoError("TAKO_UNAVAILABLE")` if it reaches `signal()`.

## Vite Plugin

Use the Vite plugin to prepare a deploy entry wrapper for Tako.

```ts
import { defineConfig } from "vite";
import { tako } from "tako.sh/vite";

export default defineConfig({
  plugins: [tako()],
});
```

On build, the plugin:

- emits `<outDir>/tako-entry.mjs`, which normalizes your compiled server module into a default-exported fetch handler

On dev (`vite dev`), the plugin:

- adds `.test`, `.tako.test`, and configured dev route hostnames to `server.allowedHosts`
- binds Vite to `127.0.0.1:$PORT` with `strictPort: true` when `PORT` is provided
- routes Vite-process `console.*`, stdout, and stderr through structured Tako app log events under `tako dev`

Deploy entry resolution uses `main` from `tako.toml`, then preset top-level `main`.
For Vite apps, point `tako.toml main` at the generated wrapper, for example:

```toml
main = "dist/server/tako-entry.mjs"
```

If your app uses Vite or another JS workspace tool behind package scripts, keep using this plugin. Tako's JS defaults run the runtime lane's `dev` / `build` scripts, so those scripts are the right place to call `vp`, `turbo`, or similar tools.

## Next.js Adapter

Use the Next.js helper to enable standalone output plus the Tako adapter:

```ts
import { withTako } from "tako.sh/nextjs";

export default withTako({
  // your existing Next config
});
```

`withTako()` configures `next/image` to use Tako's public optimizer globally:

```tsx
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="/images/hero.jpg"
      width={1200}
      height={800}
      sizes="(min-width: 1200px) 1200px, 100vw"
      alt="Hero"
    />
  );
}
```

`withTako()` aligns Next's generated image widths with Tako's default public optimizer widths so `next/image` does not request variants the server would reject.

On build, the adapter:

- forces `output: "standalone"`
- writes `.next/tako-entry.mjs`
- copies `public/` and `.next/static/` into `.next/standalone/` when Next emits standalone output

The generated wrapper prefers `.next/standalone/server.js` when it exists. If Next does not emit standalone output for the current build pipeline, the wrapper falls back to spawning `next start` against the built `.next/` directory.

For Tako projects using the `nextjs` preset, the generated deploy entrypoint is:

```toml
preset = "nextjs"
# main defaults to .next/tako-entry.mjs
```

## Build and Test

```bash
cd sdk/javascript
bun install
bun run build
bun run typecheck
bun test
```

## Related Docs

- `../../website/src/pages/docs/quickstart.md`
- `../../examples/javascript/demo/README.md`
