# LangGraph CLI compatibility

The `skein` CLI is a **drop-in replacement for the LangGraph CLI** — the migration path onto
skein-js for anyone already using it. That means two things: read the same `langgraph.json`, and
mirror the same command surface.

## Command mapping

| LangGraph CLI          | skein-js           | Behavior                                                       |
| ---------------------- | ------------------ | -------------------------------------------------------------- |
| `langgraph dev`        | `skein dev`        | In-process dev server, hot reload, **no Docker**. Local state. |
| `langgraph up`         | `skein up`         | Docker Compose stack (app + **Postgres + Redis**).             |
| `langgraph build`      | `skein build`      | Build a deployable Docker image from the config.               |
| `langgraph dockerfile` | `skein dockerfile` | Emit a standalone Dockerfile from the config.                  |
| `langgraph deploy`     | —                  | Out of scope (hosted-platform push).                           |

Shared flags where sensible: `--port`, `--host`, `--no-reload`, `--config`,
`-n, --n-jobs-per-worker`.

Production commands (`up`, `build`, `dockerfile`, and the generated `start`) also accept
`--runtime node|bun|deno` and `--runtime-version`. Precedence is CLI flag, then
`skein.runtime` in config, then Node 24 LTS. Legacy `node_version` remains the Node version fallback.

**skein-only:** `skein start` serves a pre-built `.skein/build` artifact — plain compiled JS, no vite,
no reload. It is the production image's entrypoint, and it is also how you run skein **without a
container**: `skein build --artifact-only` writes the artifact on any machine with Node, and
`skein start` serves it from a systemd unit or any process manager. See
[Without Docker](./deploy.md#without-docker). The LangGraph CLI has no equivalent — it transforms
TypeScript at runtime inside the image.

**skein-only dev flags** (beyond the LangGraph CLI) — run `skein dev` against production-shaped
storage without Docker:

| Flag                   | Values                         | Default               | Notes                                                                               |
| ---------------------- | ------------------------------ | --------------------- | ----------------------------------------------------------------------------------- |
| `--store <driver>`     | `memory`, `postgres`, `sqlite` | `memory`              | Postgres selects `PostgresSaver`; SQLite selects the upstream SQLite saver.         |
| `--queue <driver>`     | `memory`, `redis`, `sqlite`    | `memory`              | Redis provides the shared queue/bus; SQLite persists jobs in the resource database. |
| `--sqlite-path <path>` | File path                      | `.skein/state.sqlite` | Resolved against the working directory; also accepted by `start`.                   |

Graph hot-reload works with any driver; the `.skein/` snapshot is skipped for durable drivers
(they persist inherently).

SQLite is a skein deployment option for a desktop app, CLI, daemon, or one Node owner. Install
`@skein-js/local` and `@langchain/langgraph-checkpoint-sqlite@~1.0.4`, then select both drivers:

```bash
skein dev --store sqlite --queue sqlite
skein start -c .skein/build/langgraph.json --store sqlite --queue sqlite
```

Mixed SQLite/non-SQLite pairs are rejected. `dev` still defaults to memory/memory and `start` to
Postgres/Redis. SQLite requires Node 22/24/26, does not import memory snapshots, and keeps its file
outside the build artifact. Models and tools remain in the graph. See
[local deployment](./local-deployment.md) for ownership, crash behavior, and native packaging.

**Background-run concurrency.** `-n, --n-jobs-per-worker <count>` is honored on `skein dev` **and**
`skein start`, with the same default of **10** the LangGraph CLI uses — so an existing
`langgraph dev -n 4` command line ports over unchanged. `--concurrency <count>` is the same knob
under a self-evident name, and wins if both are passed. Without a flag, skein reads
`SKEIN_RUN_CONCURRENCY`, then the LangGraph-compatible `N_JOBS_PER_WORKER` — which is how the setting
reaches a container. See [runs-and-redis.md](./runs-and-redis.md#run-concurrency).

One deliberate cosmetic difference: skein's banner prints `Starting 1 worker, up to 10 concurrent
runs` where `langgraph dev` prints `Starting 10 workers`. LangGraph really does spawn 10 loops;
skein runs one worker whose consumer executes 10 runs at a time. Same throughput, honest wording. `skein up`/`build`/`dockerfile` accept `--tag` (build) and `--output`
(dockerfile) in addition to `--config`.

**`skein build --artifact-only`** (skein-only) writes `.skein/build` — the bundled graphs, the pinned
`package.json`, the baked `schemas.json`, and the generated Dockerfile — and stops without invoking
Docker. Use it when something else builds the image: Kaniko or Cloud Build in CI, a platform that
builds from a committed Dockerfile, or any machine that has Node but no Docker daemon.

**Private/authenticated npm registries.** When your production dependencies include private scoped
packages (e.g. `@myorg/*` behind a token), pass an `.npmrc` so the image's dependency install can
authenticate. `skein build --npmrc <path>` and `skein up --npmrc <path>` mount it as a BuildKit
**secret** — it authenticates the install without ever landing in an image layer or build history.
The generated Dockerfile always declares this secret mount (`id=npmrc`); it is optional, so
public-registry builds that pass no `.npmrc` are unaffected. Building the standalone `skein dockerfile`
output by hand? Supply the same secret directly:

```bash
docker build --secret id=npmrc,src=$HOME/.npmrc -t my-app .
```

`skein build`/`up` also emit a `.dockerignore` that excludes `.env*`, `node_modules`, `.git`, and
`.skein` — so secrets are **never baked into image layers**. Runtime configuration reaches the
container through the environment instead: `skein up` sets `POSTGRES_URI`/`REDIS_URI` in the compose
`environment:` block, and you add any provider keys (e.g. `OPENAI_API_KEY` for semantic search) there
too. Generated Docker assets carry a `# Generated by skein` marker; skein regenerates its own files
but leaves a hand-edited `Dockerfile`/`compose.yaml` untouched.

References:

- LangGraph CLI docs — <https://docs.langchain.com/langsmith/cli>
- `@langchain/langgraph-cli` source — <https://github.com/langchain-ai/langgraphjs/tree/main/libs/langgraph-cli>

## Migrating your project {#under-the-hood-what-skein-js-changes-transparently}

Keep your graph exports and `langgraph.json`. Replace the development command with `skein dev`,
then verify a known input in the [console](./console.md). Run your graph tests and an
[SDK smoke test](./testing-your-project.md) before switching a deployment.

TypeScript path aliases work in development and production builds, including workspace imports.
Build production artifacts with `skein build`; `skein start` serves the compiled output without a
runtime TypeScript loader. Keep your production dependencies installed in the deployed environment.

Check the [compatibility reference](./langgraph-compatibility.md) for supported client combinations
and intentional differences. CLI compatibility does not mean every feature of a managed deployment
service is included. Contributor architecture details are in the
[reuse guide](https://github.com/skein-js/skein-js/blob/main/docs/reuse.md).

## `langgraph.json` — fields we honor

skein-js parses an existing `langgraph.json` **unchanged**. A `skein.json` may extend/override
it but is never required.

```jsonc
{
  // REQUIRED: map of graph id -> "path:export"
  "graphs": {
    "agent": "./src/agent.ts:graph", // exported compiled graph instance
    "chat": "./src/chat.ts:makeGraph", // or a factory function
  },

  // JS/Node runtime pin (20 | 22 | 24)
  "node_version": "24",

  // Native production runtime (skein extension). Omit for Node.
  "skein": {
    "runtime": { "name": "bun", "version": "1.3.14" },
    // Retention for `Idempotency-Key` records (skein extension; see below). Tuning only —
    // omitting the block does NOT disable the header.
    "idempotency": { "retention_hours": 24, "in_flight_minutes": 15 },
    // Delivery policy for run-completion callbacks (skein extension; see below). Tuning only —
    // omitting the block does NOT make callbacks fire-once.
    "webhooks": { "retries": { "max_attempts": 12 }, "allowed_hosts": ["hooks.example.com"] },
  },

  // .env path OR inline map
  "env": ".env",

  // long-term memory store; semantic search config drives our pgvector index
  "store": {
    "index": { "embed": "openai:text-embedding-3-small", "dims": 1536, "fields": ["$"] },
    // your own LangGraph BaseStore instead of skein's driver (skein extension)
    "adapter": "./src/my-store.ts:store",
  },

  // checkpointer backend; "default" == Postgres (via PostgresSaver)
  "checkpointer": { "type": "default" },

  // server customization — `cors` plus LangGraph's route toggles
  "http": {
    "cors": { "allow_origins": ["*"] },
    "disable_assistants": false,
    "disable_threads": false,
    "disable_runs": false,
    "disable_crons": false, // skein-only: also stops the scheduler, not just the routes
    "disable_store": false,
    "disable_docs": false, // hide /docs and /openapi.json while keeping /info and /metrics
    "disable_meta": false, // turns off /info, /openapi.json, /docs and /metrics; never /ok
    "console": false, // skein-only: serve the console at /console (or a path). Off unless set;
    //                   `skein dev` serves it anyway. See docs/console.md.
    "app": "./src/http-app.ts:app", // Fetch function or exported Hono/Fetch application
    "disable_mcp": false,
    "disable_a2a": false,
  },

  // custom authentication + authorization (see below)
  "auth": {
    "path": "./src/auth.ts:auth", // a "@langchain/langgraph-sdk/auth" `Auth` instance
    "disable_studio_auth": true, // default; CLI always requires credentials for Studio
    "dev_ui_access": false, // skein-only; dev key for console + Studio when true
  },

  // where runs report themselves — traces + lifecycle events (a skein extension; see below)
  "telemetry": {
    "langsmith": true,
    "posthog": { "host": "https://eu.i.posthog.com" },
    "otel": true,
    "paths": ["./src/my-telemetry.ts:sink"], // your own TelemetrySink
  },

  // packages your code loads BY NAME at runtime, which no bundler can discover (skein extension)
  "dependencies": ["@langchain/openai"],

  // extra Dockerfile lines appended after the base image
  "dockerfile_lines": [],
}
```

### How each field maps into skein-js

| `langgraph.json` field | skein-js wiring                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graphs`               | [`@skein-js/config`](./storage.md) resolves each `path:export`, loading a compiled graph or `makeGraph` factory. Drives `/agents` introspection + run execution.                                                                                                                                                                                                                                                                                                              |
| `node_version`         | Used by `skein build` / `skein dockerfile` base image selection. Defaults to Node 24 LTS when omitted; an explicit value is honoured verbatim, including an older one.                                                                                                                                                                                                                                                                                                        |
| `skein.runtime`        | Native production server and pinned official image: `node`, `bun`, or `deno`. Bun/Deno use `@skein-js/fetch` with `Bun.serve`/`Deno.serve`, never Express compatibility.                                                                                                                                                                                                                                                                                                      |
| `skein.idempotency`    | Retention for `Idempotency-Key` records. Tuning only — the header is honoured either way. See [below](#idempotency-skeinidempotency).                                                                                                                                                                                                                                                                                                                                         |
| `skein.webhooks`       | How run-completion callbacks are retried and bounded. Tuning only — a run carrying a `webhook` owes a callback either way. See [below](#webhooks-skeinwebhooks).                                                                                                                                                                                                                                                                                                              |
| `env`                  | Loaded into `process.env` at boot (dev) / baked into the image (build).                                                                                                                                                                                                                                                                                                                                                                                                       |
| `store`                | `store.index.{embed,dims,fields,hnsw}` configures pgvector semantic search on the Postgres driver (`hnsw: true` opts into the approximate index); `store.ttl.{default_ttl,refresh_on_read,sweep_interval_minutes}` expires items. `store.adapter` is a **skein extension** — `path:export` to your own LangGraph `BaseStore` (e.g. `PostgresStore`, `MongoDBStore`), which then serves the whole `/store` surface. See [storage.md](./storage.md).                            |
| `checkpointer`         | `"default"` → `PostgresSaver`; dev falls back to an in-memory `MemorySaver`.                                                                                                                                                                                                                                                                                                                                                                                                  |
| `http`                 | `http.cors` maps to the adapter's CORS options; the `disable_*` flags remove that resource's routes before mounting, so it 404s from the host app as under `langgraph dev`. `http.console` is skein-only — it serves the [console](./console.md) at `/console` (off unless set; `skein dev` serves it regardless). `http.app` loads an exported Fetch/Hono application before protocol dispatch; a 404 falls through. Authentication runs before custom application dispatch. |
| `auth`                 | `auth.path` loads an `Auth` from `@langchain/langgraph-sdk/auth`; every request is authenticated + authorized. The CLI requires credentials for Studio regardless of `disable_studio_auth`; `auth.dev_ui_access: true` or loopback `skein dev --dev-ui-access` provides a temporary key for Studio and the console. The setting is stripped from production builds.                                                                                                           |
| `telemetry`            | **skein extension.** Builds the telemetry sinks runs report to — see [observability.md](./observability.md). Unknown to `langgraph dev`, which ignores it.                                                                                                                                                                                                                                                                                                                    |
| `dependencies`         | **skein extension on the JS side** (LangGraph's schema has it for Python only). Extra packages `skein build` pins into the artifact — see below.                                                                                                                                                                                                                                                                                                                              |
| `dockerfile_lines`     | Appended by `skein dockerfile` / `skein build`.                                                                                                                                                                                                                                                                                                                                                                                                                               |

**`dependencies` means something different here.** In LangGraph it is a Python-only field, required
and used to drive `pip install`; the JS schema has no such field, because `langgraph build` copies
your project into the image and runs your package manager over it. skein bundles instead, so it
derives the image's dependency list from what the bundle still imports — and `dependencies` is the
escape hatch for the packages that list cannot contain: the ones you load **by name at runtime**
(`import("@langchain/" + provider)`), which never appear in any module graph. Local-path entries
(`"."`, `"./pkg"`) are accepted and ignored — that source is already bundled. See
[bundling.md](./bundling.md#what-skein-build-inlines-vs-externalizes).

## Graph loading (`path:export` notation)

`@skein-js/config` resolves entries exactly like the LangGraph CLI:

- `"./src/agent.ts:graph"` — imports the module and reads the `graph` export, which must be
  a `CompiledStateGraph`.
- `"./src/agent.ts:makeGraph"` — reads a factory export and calls it (optionally with
  config) to obtain a `CompiledStateGraph`.

This is the same contract LangGraph.js users already write against, so **no code changes
are required** to move a project onto skein-js.

<span id="idempotency-skeinidempotency"></span>

## Idempotency (`skein.idempotency`)

A skein extension with no LangGraph counterpart, so it lives under the reserved `skein` namespace
rather than at the top level — a top-level key would collide if LangGraph ever claimed the same name.

```jsonc
{
  "skein": {
    "idempotency": {
      "retention_hours": 24, // how long a recorded response stays replayable
      "in_flight_minutes": 15, // how long an unfinished create blocks a retry
      "sweep_interval_minutes": 60, // how often expired records are reclaimed
    },
  },
}
```

**This block is tuning, not an on/off switch.** Omitting it does not disable `Idempotency-Key`: a
caller who sends one has been promised their retry will not start a second run, and honouring that is
not a deployment opinion. The defaults are the values shown above.

What each one is for:

- `retention_hours` — the dedup window. Long enough to cover any provider's retry schedule, short
  enough that the table doesn't grow without bound. Raise it if you integrate a provider that retries
  over days.
- `in_flight_minutes` — how long a create that hasn't finished holds its key before a retry may take
  it over. Keep it above your slowest `POST /runs/wait`, or a retry could be admitted alongside the
  original; keep it well under `retention_hours`, or an instance killed mid-request leaves its keys
  409-ing until the retention expires.
- `sweep_interval_minutes` — cadence of the background reclaim. Purely housekeeping: an expired
  record is taken over by the next claim whether or not the sweeper has run, so this affects disk,
  never correctness.

Honoured identically on every driver combination, including `skein dev` with no Docker. See
[agent-protocol.md](./agent-protocol.md#idempotent-run-creation-idempotency-key) for the request
semantics and the replay table.

<span id="webhooks-skeinwebhooks"></span>

## Webhooks (`skein.webhooks`)

LangGraph's `webhook` field is a bare URL with no delivery policy at all, so this block is a skein
extension under the reserved namespace, for the same reason `skein.idempotency` is. The **payload
shape is unchanged** — this configures delivery, not the body.

```jsonc
{
  "skein": {
    "webhooks": {
      "retries": {
        "max_attempts": 12, // attempts before a callback is dead, counting the inline first one
        "initial_delay_ms": 1000, // the first retry's delay; the rest double from it
      },
      // The signing key. Prefer SKEIN_WEBHOOK_SECRET, which wins over this — see below.
      "secret": "whsec_…",
      "max_payload_bytes": 262144, // cap on the stored body; over it, `values` is truncated
      "retain_hours": 24, // how long a settled delivery is kept before it is reclaimed
      "allowed_hosts": ["hooks.example.com"], // absent = no restriction (today's behaviour)
      "require_https": true, // refuse plaintext callbacks; absent permits them
    },
  },
}
```

**This block is tuning, not an on/off switch.** Omitting it does not make callbacks fire-once: a run
that carries a `webhook` owes a callback, and how hard the server tries is a deployment decision
rather than permission to try at all. `retries.max_attempts: 1` is how you ask for one shot.

What each one is for:

- `max_attempts` — read it as a **time horizon, not a count**. The delays double, so 12 attempts is
  1+2+4+…+1024 seconds ≈ **34 minutes**, which rides out a rolling deploy with room to spare. Six is
  not "half as patient": it is ~31 seconds, which does not survive one redeploy. There is deliberately
  no ceiling knob — the doubling tops out around 17 minutes at any sane attempt count, and on Redis
  the schedule is BullMQ's own exponential backoff rather than ours.
- `initial_delay_ms` — the first retry's delay and the base the rest double from. Raise it for a
  receiver you know is slow to come back; lower it only if you also lower `max_attempts`.
- `max_payload_bytes` — the body is **stored** so a retry has something to send, so an unbounded
  payload is unbounded rows. Over the cap, `values` is replaced by a truncation marker inside the
  signed body; everything a receiver needs to fetch the state itself survives.
- `retain_hours` — how long a delivered or dead row is kept. Disk only; nothing is incorrect if the
  sweep never runs.
- `secret` — the signing key, or a list during a rotation (the first signs). **Prefer the
  `SKEIN_WEBHOOK_SECRET` environment variable**, which takes precedence: a key here is a key in
  version control, and skein warns about it once at startup. Note that skein does **not** expand
  `${VAR}` anywhere in `langgraph.json`, so `"secret": "${SKEIN_WEBHOOK_SECRET}"` would sign every
  callback with that literal 23-character string. Absent on both → callbacks are unsigned, which is
  today's behaviour.
- `require_https` — refuse a callback whose URL is not `https:`. Off by default, because an internal
  receiver on a trusted network is legitimate and turning it on for everyone would break those
  deployments on upgrade. Turn it on whenever callbacks leave your network: the body carries the run's
  final state, and retries mean it crosses the wire up to `max_attempts` times rather than once.
- `allowed_hosts` — an exact-hostname allowlist. Set it if you accept run creates from untrusted
  callers: `webhook` is a caller-supplied URL, so it is a server-side request to a target they chose,
  and retrying it turns a one-shot SSRF probe into a repeated one. **Off by default**, because turning
  it on for everyone would make an upgrade start dropping deployments' own callbacks. A refused host
  is recorded `dead` with the reason rather than silently skipped.

**Where the retries actually run depends on your queue driver.** With Redis, the whole schedule is
BullMQ's — delayed jobs, exponential backoff with jitter, and re-delivery of a job whose worker was
killed — so a retry still waiting survives a restart. Without it, skein polls the outbox instead:
the stored due time and delivery lease survive restart whenever the store is durable. SQLite and
Postgres therefore retain waiting retries without a separate delivery queue.

With a durable store, the delivery and its due time are recorded in the same transaction as the
run's terminal status. Waiting retries therefore survive restart without a separate queue. See
[webhooks.md](./webhooks.md).

## Authentication + authorization (`auth`)

skein-js implements LangGraph's **custom-auth** model. Point `auth.path` at a module exporting a
[`@langchain/langgraph-sdk/auth`](https://github.com/langchain-ai/langgraphjs/tree/main/libs/sdk) `Auth`
instance — the same class LangGraph Platform uses, so an existing auth file is drop-in:

```ts
import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth";

export const auth = new Auth()
  .authenticate(async (request) => {
    const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
    const user = token ? await verify(token) : undefined;
    if (!user) throw new HTTPException(401, { message: "Unauthorized" });
    return { identity: user.id, permissions: user.scopes }; // becomes ctx.user
  })
  .on("threads", ({ user }) => ({ owner: user.identity })); // scope threads (and their runs) by owner
```

- `.authenticate(...)` runs on every request; throwing an `HTTPException` (or returning nothing) is a
  `401`. Its return becomes the `user` passed to the `.on(...)` handlers.
- `.on("<resource>:<action>", ...)` (or a broader `"threads"` / `"*"`) authorizes each operation:
  return `false` for a `403`, or a **filter** object to scope the resource. A returned filter both
  hides other owners' rows on reads and stamps ownership onto new rows. Runs authorize through their
  owning thread (there is no separate `runs` resource), matching LangGraph.
- **No `auth` block** → the server is fully open (unauthenticated), exactly as before.
- Studio's `x-auth-scheme: langsmith` header is caller-controlled, so the CLI requires credentials for
  Studio even when `disable_studio_auth` is `false`. `skein dev --dev-ui-access` or
  `auth.dev_ui_access: true` prints a temporary API key accepted by Studio and the console on a
  loopback server. `skein start` never enables it, and production builds strip the setting.
  Direct config loaders also default `disable_studio_auth` to `true`; an explicitly injected engine
  controls its own policy.

See [agent-protocol.md](./agent-protocol.md#authentication--authorization) for the full request
lifecycle and the route → resource/action map.

## `dev` vs `up`

- **`skein dev`** — a single Node app process with hot reload. It defaults to in-memory
  (optionally file-backed) state, so Docker is not required; pass
  `--store postgres --queue redis` to develop against the production drivers, including Postgres
  and Redis running in Docker. Fast feedback, the exact `langgraph dev` niche.
- **`skein up`** — Docker Compose bringing up the app plus Postgres (checkpoints + protocol
  resources + pgvector) and Redis (queue + cross-instance streaming). Mirrors production —
  see [runs-and-redis.md](./runs-and-redis.md).

## Migrating an existing `langgraph dev` state

When you run `langgraph dev`, LangGraph persists all local state — assistants, threads, runs,
store items, and graph **checkpoints** (state + full history) — under a `.langgraph_api/`
directory. skein reads that format and carries everything over, so switching loses nothing.

**Automatic (the common case).** The first time you run `skein dev` in a project that has a
`.langgraph_api/` directory but no skein state yet (`.skein/dev-state.json`), skein imports it on
boot and logs what it brought over. From then on it persists to `.skein/` as usual, so the import
runs exactly once. Nothing to do — your old threads and their history are just there.

**Explicit — `skein import-langgraph`.** For re-running, previewing counts, or importing into a
durable database:

| Flag               | Values               | Default                    | Notes                                                                                 |
| ------------------ | -------------------- | -------------------------- | ------------------------------------------------------------------------------------- |
| `-c, --config`     | path                 | `langgraph.json`           | Resolves the project directory.                                                       |
| `--store <driver>` | `memory`, `postgres` | `memory`                   | `memory` writes `.skein/dev-state.json`; `postgres` loads a live DB (`POSTGRES_URI`). |
| `--from <dir>`     | path                 | `<project>/.langgraph_api` | Source directory to read.                                                             |
| `--force`          | —                    | off                        | Overwrite an existing `.skein/dev-state.json` (memory target).                        |

```bash
skein import-langgraph                     # → .skein/dev-state.json, then `skein dev`
skein import-langgraph --store postgres     # → your POSTGRES_URI (checkpoints + resource rows)
```

**What carries over, and what doesn't.** Assistants (with their **version history**), threads (with
their latest state), runs, store items, and the **full checkpoint history** are all migrated. Because
skein uses LangGraph's own `MemorySaver`/`PostgresSaver`, the checkpoint format is identical — history
is preserved exactly. Minor, deliberate drops: the store's derived **embedding index** (`vectors`) is
not copied (skein re-indexes on write / a configured `store.index` re-embeds on Postgres import), and
LangGraph **retry counters** aren't part of skein's model. Run **webhooks** _are_ carried through, so
an imported run still fires its completion webhook.

> The `.langgraph_api/` layout is a `@langchain/langgraph-api` internal (stable across 1.2.x–1.4.x),
> not a public API. The importer is best-effort and guarded — a format it can't read never blocks
> `skein dev` from starting.
>
> **Already on Postgres?** If your LangGraph deployment already uses a Postgres checkpointer, you
> don't need to migrate checkpoints at all: point skein at the **same `POSTGRES_URI`** and it shares
> the exact same checkpoint tables. `skein import-langgraph --store postgres` is for moving an
> in-memory `langgraph dev` (including one run in production) onto a durable skein deployment.

## Graph UI and custom HTTP applications

Standard `ui` and `ui_config` use the public `@langchain/langgraph-ui` build/watch API. For example:

```json
{
  "graphs": { "agent": "./src/agent.ts:graph" },
  "ui": { "agent": "./src/ui.tsx" },
  "ui_config": { "shared": ["react", "react-dom"] },
  "http": { "app": "./src/http-app.ts:app" }
}
```

A UI source exports a default component registry. Development watches the sources; `skein build`
compiles assets, schemas and the custom application into the artifact. Production serves
`POST /ui/{agent}` bootstrap HTML and `GET /ui/{agent}/{asset}` from those compiled assets without
loading the UI compiler or project sources. Application exports may be a Fetch function or an object
with `fetch(request)`, including Hono. They return a standard `Response`; a 404 continues to UI and
Agent Protocol routes, with the original request body available even if the app read its copy.
All five adapters share this behavior, including mounted path prefixes.

Deployment authentication runs before custom routes and UI dispatch, preserving the mounted
protocol routes’ existing exemptions: the `/info` handshake is public, and channel callbacks use
provider-signature verification. When rendering external UI
scripts or styles in a browser, use credentials the browser can supply on asset requests (such as
same-origin cookies), or explicitly allow those asset paths in the deployment's auth policy.
`auth.openapi` may describe that existing policy using standard `securitySchemes` and `security`;
it does not change authentication or authorization.
