---
title: Security
description: Authenticate the management API with the authorize hook, understand which routes are exempt, and harden secrets, cron, and debug for production.
section: Get Started
url: /docs/security
---

# Security

khotan mounts a single catch-all route at `/api/khotan/[...all]` that powers the dashboard, CLI, and runtime. That route exposes management endpoints for plugs, flows, variables, wires, webhook handlers, runs, mappings, caches, and resources.

**By default, that management API is unauthenticated.** On a deployed app, anyone who can reach `/api/khotan/*` can read and write plug credentials, trigger flows, and edit mappings. You secure it with the `authorize` hook.

## The `authorize` hook

Pass an `authorize` function to the `khotan()` factory. It receives the raw `Request` and returns `true` to allow the request or `false` to reject it with `401`.

```ts
export type KhotanAuthorize = (
  request: Request,
) => boolean | Promise<boolean>;
```

The hook runs inside the catch-all handler on every management request, before any route logic. It is not middleware — there is nothing to add to `middleware.ts`, and it works on any host.

```ts
const khotanData = khotan({
  adapter: drizzleAdapter(db),
  authorize: async (request) => {
    const session = await auth.api.getSession({ headers: request.headers });
    return Boolean(session?.user);
  },
});
```

Because you get the raw `Request`, you can authorize however your app already does — session cookies, bearer tokens, API keys, or role checks.

```ts
// Restrict the management API to admins only
authorize: async (request) => {
  const session = await auth.api.getSession({ headers: request.headers });
  return session?.user?.role === "admin";
},
```

If the hook throws, the request is treated as unauthorized and rejected with `401`.

## What it gates

When `authorize` is set, every management route must pass it: `plugs`, `flows`, `variables`, `wires`, `webhook-handlers`, `runs`, `mappings`, `caches`, and `resources`.

## What is exempt

Three route families skip the `authorize` hook because they carry their own protection:

- **Inbound webhooks** (`POST .../webhook/:plug`) — verified per-plug via the wire's `onVerify` signature check.
- **The cron dispatcher** (`.../cron`) — protected by `CRON_SECRET`.
- **Debug routes** (`.../debug...`) — gated by `KHOTAN_DEBUG` and disabled in production.

If you want those behind your own auth too, that is a config decision — they are intentionally exempt by default so external callers (webhook providers, platform cron) can reach them.

## The 401 response

A rejected request returns:

```json
{ "error": "Unauthorized" }
```

with HTTP status `401`. The scaffolded dashboard components (`Hub`, debug, logs, mappings, panels) route every API call through a shared `khotanFetch` helper and render a simple `ApiErrorState` on failure — so an unauthenticated user sees a clean "Access denied" state instead of a blank screen. The same component handles `403`, `404`, `429`, `5xx`, and network errors. It lives in `components/khotan/api-state.tsx` and is yours to restyle.

## CLI access

The khotan CLI talks to your running dev server over `/api/khotan/*`, so it passes through the same `authorize` gate. To avoid forcing a terminal login, the CLI authenticates with a token **derived from `KHOTAN_SECRET`** — it never sends the raw secret:

- It sends a timestamped HMAC of `KHOTAN_SECRET` as `Authorization: KhotanCLI <timestamp>.<hmac>`. Because it's a one-way hash, the encryption key never travels over the wire.
- The factory accepts this token **only when `NODE_ENV !== 'production'`** and only within a ~60s freshness window (so a token captured from a dev log can't be replayed).
- So `khotan plug`, `plug vars`, `flows`, `mappings`, and `wire` work against your local dev server as long as `KHOTAN_SECRET` is set in your environment or `.env.local`.

Commands that only touch the filesystem or run migrations (`init`, `add`, `generate`, `migrate`) never call the API and are unaffected. In production the CLI token is rejected, so the deployed management API is governed solely by your `authorize` hook.

## Secrets and hardening

- `KHOTAN_SECRET` — encrypts plug credentials and wire metadata at rest. Without it, those values are stored unencrypted. Treat it like any other production secret; if you lose it you lose access to encrypted vars.
- `CRON_SECRET` — protects the `/api/khotan/cron` dispatcher. It **fails closed in production**: if it is unset on a deployed app, the dispatcher is rejected. In development it stays open for local testing.
- `KHOTAN_DEBUG` — enables the debug routes. They are **disabled in production** regardless of this flag, so the credentialed debug proxy can never be reached on a deployed app.

## Startup warnings

The factory logs a one-time warning at construction if `authorize` is not configured (the management API is public) or if no `KHOTAN_SECRET` is set (credentials are unencrypted). Treat both as required for any deployed app.

## Production checklist

- Set an `authorize` hook that reflects who should manage khotan.
- Set `KHOTAN_SECRET` to a random 32+ byte value.
- Set `CRON_SECRET` and have your platform cron send `Authorization: Bearer <CRON_SECRET>`.
- Leave `KHOTAN_DEBUG` unset in production.
- Confirm unauthenticated requests to `/api/khotan/plugs` return `401`.
