# Approve from anywhere

A run pauses at a write and emails the approver an id, not a link; they open a compact approval page, decide against the change's hash (editing it first if they need to), and the run resumes, even after a server restart.

- Registry name: `approve-from-anywhere` (`@candor/approve-from-anywhere`)
- Tier: Premium. Installing it needs a Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/approve-from-anywhere

## Install

```bash
npx candor-ui add approve-from-anywhere
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/approve-from-anywhere`.
It brings in: approval-request, button, change-hash, chat, composer, email-layout, entity-chip, input, message, prompt-suggestions, thread, use-candor-chat, work.
npm packages: @ai-sdk/anthropic@^4.0.96, @ai-sdk/react@^4.0.166, ai@^7.0.162, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7.

It installs under your app's `app/` folder and runs with a server and an agent: set `ANTHROPIC_API_KEY`, and read the comment at the top of its `store.ts` for storage and auth. Its files:

- `app/approve-from-anywhere/api/approvals/route.ts`
- `app/approve-from-anywhere/api/outbox/route.ts`
- `app/approve-from-anywhere/api/route.ts`
- `app/approve-from-anywhere/approve/page.tsx`
- `app/approve-from-anywhere/db/connect.ts`
- `app/approve-from-anywhere/db/drizzle-repo.ts`
- `app/approve-from-anywhere/db/schema.ts`
- `app/approve-from-anywhere/db/seed.ts`
- `app/approve-from-anywhere/decisions.ts`
- `app/approve-from-anywhere/page.tsx`
- `app/approve-from-anywhere/repo.ts`
- `app/approve-from-anywhere/rollout.ts`
- `app/approve-from-anywhere/store.ts`
- `app/approve-from-anywhere/ui.tsx`

## <ApprovePage>

The approver's side, made for a phone: they type the id from their email (it has no link), see the change in brief with its hash, open it in full to edit or to decide a production change, and decide. The run that asked carries on by itself.

No props.

## <Page>

No props.

## <SignedInAs>

The demo's "signed in as" switch: sets the cookie viewerOf() reads.

Props:

- `viewer?: string | undefined`
- `onChange?: (() => void) | undefined`

## <ApproveFromAnywhere>

The person who asks: a chat with an agent that changes feature-flag rollouts. Its one write pauses the run and emails someone else an id; while it waits, the run's approval shows here, read-only, and the page can be closed.

Props:

- `chat: RolloutChat`

## Functions

### connectAnywhereDb

```ts
connectAnywhereDb(url: string)
```

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

### drizzleAnywhereRepo

```ts
drizzleAnywhereRepo(db: AnywhereDb)
```

### seedAnywhere

```ts
seedAnywhere(db: AnywhereDb)
```

Load the demo's flags into the database, leaving any that already exist alone. Run it once after migrating, e.g. from a script: `await seedAnywhere(connectAnywhereDb(process.env.DATABASE_URL!))`.

### getApprovals

```ts
getApprovals(request: Request)
```

`?id=AP-002`: one approval, with the percentage it starts from (the approval page needs it to hash an edit). `?runId=…`: the run's approvals, for the person who started it.

### postDecision

```ts
postDecision(request: Request)
```

`{ id, hash, approved, note?, edited?: { input, hash } }`. Returns the response and, when it was recorded, the decision.

### memoryAnywhereRepo

```ts
memoryAnywhereRepo()
```

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from.

### rolloutChange

```ts
rolloutChange(input: Pick<RolloutInput, "environment" | "flag" | "percent">, from: number)
```

The change an input makes, starting from the flag's value when it was asked for.

### rolloutHash

```ts
rolloutHash(change: RolloutChange)
```

The hash an approval is bound to. Recomputed on the server for every decision, and again before the change applies.

### setAnywhereRepo

```ts
setAnywhereRepo(repo: AnywhereRepo)
```

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

### anywhereRepo

```ts
anywhereRepo()
```

The storage in use: whatever setAnywhereRepo() was given, else Postgres when DATABASE_URL is set, else memory.

### viewerOf

```ts
viewerOf(request: Request)
```

Who is asking. Replace with your auth; the demo reads a cookie the pages set.

### readRollouts

```ts
readRollouts(environment: Environment)
```

### refusal

```ts
refusal(input: unknown)
```

Why a proposed rollout can't be asked for, or undefined when it can. Checked before the run pauses for it.

### requestApproval

```ts
requestApproval(a: { sdkId: string; runId: string; requester: string; tool: string; callId: string; input: unknown; request?: unknown })
```

Record a write that waits on a person, and email the people who can approve it. Called by the SDK's approval request: the AI SDK route's onFinish, or the Claude ApprovalStore's `put`.

### approvalEmail

```ts
approvalEmail(r: ApprovalRecord)
```

The email an approver gets: the change in words, the id to type and the hash to check. Never a link to click.

### toApprovalRequest

```ts
toApprovalRequest(r: ApprovalRecord, viewer: string)
```

The stored approval as the approval page shows it, for one viewer: the requester can't decide their own.

### approvalFor

```ts
approvalFor(id: string, viewer: string)
```

One approval for the approval page, by the id from the email. Undefined when there's none.

### expireOverdue

```ts
expireOverdue()
```

Pending approvals past their expiry become expired, so nobody can decide them and the run is told.

### decide

```ts
decide(d: Decision, viewer: string)
```

Approve or reject exactly the change the approver saw, identified by its hash. An edit must keep the environment and the flag, and its hash is worked out again here: the approval binds to the edited change only if it matches the one the approver was shown.

### applyApproved

```ts
applyApproved(id: string)
```

Apply an approved write, once, exactly as approved, and only if the flag is still where it was when it was asked for.

### outcomeOf

```ts
outcomeOf(id: string)
```

What the write tool returns to the agent once the run has a decision on it, in a sentence: applied (and how), rejected (and the approver's note), or expired. Applies the change when it's approved.

### approvalForCall

```ts
approvalForCall(runId: string, input: unknown)
```

The approved write that covers this tool call, for the Claude tool handler, which gets the input but not the approval's id.

### claimResume

```ts
claimResume(id: string)
```

Take the right to resume a run with this decision. One process gets it; a claim left unfinished for minutes is taken again.

### markResumed

```ts
markResumed(id: string)
```

The run has the decision.

### runOf

```ts
runOf(id: string)
```

### startRun

```ts
startRun(id: string, owner: string, sdk: RunRecord["sdk"])
```

Start a run, or check the viewer owns the one they're continuing. Returns why not, when they can't.

### keepAlive

```ts
keepAlive(runId: string, everyMs = 5_000)
```

Beat while this process runs the run, so another can tell when it went away. Returns the function that stops it.

### endRun

```ts
endRun(runId: string)
```

### waitingToResume

```ts
waitingToResume()
```

Approvals decided (or expired) that their run doesn't have yet, with the run's record.

### resetStore

```ts
resetStore()
```

For tests: start from the seed data again. On a database this deletes the block's tables' rows.

### rolloutToolTitle

```ts
rolloutToolTitle(tool: string, input: unknown)
```

Plain-words titles for the block's tools, the same for both agent SDKs.

### useRunApprovals

```ts
useRunApprovals(runId: string | undefined, poll: boolean)
```

A run's approvals as its owner sees them, checked every few seconds while one is waiting.

### useSavedRun

```ts
useSavedRun(key: string)
```

The run this browser last started, kept so a reload (or coming back tomorrow) picks it up again.

## Types

### AnywhereDb

```ts
PgDatabase<PgQueryResultHKT, any>
```

### ApprovalRecord

One write waiting for, or given, a person's decision.

```ts
{ /** AP-001, AP-002, …: the id in the email, typed on the approval page. */ id: string /** The SDK's own id for the request: the AI SDK's approval id, or the Claude ApprovalStore's. */ sdkId: string runId: string /** Who started the run. They can't approve their own run's writes. */ requester: string /** The tool call: its name, the SDK's id for the call, and the input as the agent proposed it. *
```

### NewApprovalRecord

```ts
Omit<ApprovalRecord, "id" | "state">
```

### ApprovalPatch

```ts
Partial<Omit<ApprovalRecord, "id" | "sdkId" | "runId" | "requester" | "tool" | "callId" | "input" | "hash" | "createdAt">>
```

### RunRecord

A run: who started it, which SDK, and what it has said so far.

```ts
{ id: string owner: string sdk: "ai-sdk" | "claude" /** AI SDK: the whole conversation (UIMessage[]), saved after every step. */ messages?: unknown[] | undefined /** Claude: the SDK's session, so a run can be resumed after a restart. */ sessionId?: string | undefined startedAt: string /** Updated every few seconds by the process running it. A run not ended whose heartbeat stopped was cut off. */ h
```

### OutboxEmail

```ts
{ seq: number; to: string; approvalId: string; notice: EmailNotice; html: string; at: string }
```

### Environment

```ts
(typeof environments)[number]
```

### RolloutInput

The write tool's input: a feature flag's rollout, as a percentage of users, in one environment.

```ts
{ environment: Environment; flag: string; percent: number; reason?: string | undefined }
```

### RolloutChange

What a rollout change does, with the value it starts from. The hash covers exactly this, not the reason.

```ts
{ environment: Environment; flag: string; from: number; to: number }
```

### Decision

```ts
{ id: string /** The hash the approver saw: the proposed change's. */ hash: string approved: boolean note?: string | undefined /** The input as the approver edited it, with the hash they saw for it. */ edited?: { input: Record<string, unknown>; hash: string } | undefined }
```

### RolloutChat

What either page gives the chat: the adapter hook's messages and composer, the run's approvals, and starting over.

```ts
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> error?: string | undefined /** The run's approvals, from the server. */ approvals: ApprovalData[] onNewRun: () => void }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
