---
title: Controllers
description: The four hooks behind the flow, the tagged states they expose, the commands they take, and how an attempt is abandoned when its inputs change.
sidebar:
  label: Controllers
seo:
  title: DomainKit React controllers
---

Use these hooks when you build the domain surface yourself. Each part of the flow is a hook that
takes one options object and returns a controller whose `state` is a tagged union, so a screen renders from `state._tag` and never from a boolean pair.

<Snippet file="examples/react/controllers.tsx" region="hooks" title="The four controllers" />

## Connection

`Connect.useController({ domain, returnTo?, readOnly? })`

| State               | Means                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `Loading`           | Inspecting the domain                                                                     |
| `Disconnected`      | No connection. Carries `discovery` when one of the owner's connections reaches the domain |
| `Connected`         | Attached and usable                                                                       |
| `Reconnect`         | The credential can no longer be refreshed                                                 |
| `Submitting`        | A connect, attach, detach, or disconnect is in flight                                     |
| `Redirecting`       | Sending the customer to the provider                                                      |
| `SelectionRequired` | The credential reaches several matching zones                                             |
| `Failure`           | Carries the `DomainKit.Error`                                                             |

| Command                                             | Does                                                                                            |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `connect({ provider, method, values?, returnTo? })` | Starts a connection, or proves the same provider again for a connection that needs reconnecting |
| `reuse({ connectionId, zone? })`                    | Attaches the domain to a connection the owner already has                                       |
| `select(zone)`                                      | Answers `SelectionRequired`                                                                     |
| `detach()` / `disconnect()`                         | Releases the domain, or the whole connection                                                    |
| `refresh()` / `retry()`                             | Re-inspects, or re-runs the step that failed                                                    |

Discovery runs on mount whenever the transport declares it and the domain has no connection yet. A
discovery failure leaves `discovery` null and the provider list still renders: discovery is an
optimisation, not a step the customer asked for.

`returnTo` is where an interactive method sends the customer back to. It defaults to the page they
started from, read when they connect rather than when the controller renders, so a flow mounted on
one screen and used on another still returns to the right place. Pass `null` to send none and leave
the server's `defaultReturnTo` in charge; a per-call `connect({ returnTo })` still wins.

The controller attaches on sight: when it settles `Disconnected` with a `Resolved` discovery on a
surface that is not read-only, it calls `reuse` for that connection and zone straight away, so a
second domain on an account the workspace already holds never meets the connect dialog. Two
connections that both reach the zone stay a decision, and `SelectionRequired` keeps the prompt.
Each domain, connection, and zone is tried once, so an attach that failed and a detach the customer
chose both stand.

`receipt` carries what this domain's last apply landed, read once per receipt id, so the connected
card can say how many records it holds without another call.

## Zones and accounts

`Connect.useZones({ provider? })` lists every zone the owner's connections reach. Its state is
`Loading`, `Ready { zones, connections, providers }`, or `Failure`; a connection whose credential
the provider turned down appears in `connections` as `reconnect` and contributes no zones. Each
connection carries the account `label` its provider gave at connect time, so a list names accounts
without a provider call; it is `null` when the provider named no single account.

`Connect.useAccounts({ returnTo?, readOnly? })` reads the same listing and adds two commands:
`connect({ provider, method, values? })` starts a provider with no domain attached, and
`reconnect({ connectionId, provider, method, values? })` replaces the credential behind a
connection the owner already holds, keeping its domains. A token method finishes in place; an
interactive one redirects and returns through the callback route your server mounts. The registry's [`domain-field`](/components/registry/domain-field) is a surface over both.

## Provisioning and cleanup

`Provision.useController({ domain, requirements, onApplied?, readOnly? })` and
`Cleanup.useController({ domain, receiptId?, onCleaned?, readOnly? })` run the same machine.

| State       | Means                                      |
| ----------- | ------------------------------------------ |
| `Idle`      | Nothing planned yet                        |
| `Planning`  | Building the plan                          |
| `Planned`   | Carries the plan, waiting for the customer |
| `Approving` | Recording consent                          |
| `Applying`  | Writing                                    |
| `Applied`   | Carries the receipt, complete or partial   |
| `Rejecting` | Recording a refusal                        |
| `Rejected`  | Carries the closed attempt                 |
| `Failure`   | Carries the `DomainKit.Error`              |

<Snippet file="examples/react/controllers.tsx" region="commands" title="Approve or decline" />

`approve(operationIds?)` authorizes the digest and applies it in one customer action, because the
review screen offers one approve and one decline rather than approve and then apply. `apply()` uses
the approval this controller already holds; it does nothing until both a plan and an approval are
available. `reject(reason?)` is terminal: approving that plan afterwards fails `Stale`.

`retry()` builds a new plan when the reason says the old one is gone (`Stale`, `Expired`,
`Conflict`) and re-runs the failed step otherwise.

## Verification

`Verify.useController({ domain, polling?, requirements?, supplied? })` observes on mount when
`supplied` is absent, then re-observes at the readiness row's own `nextCheckAt` while polling is
enabled and it stays mounted. `readiness` holds the latest observation even while a new one runs,
so the table does not blink. With supplied readiness, it observes nothing on mount and sets no timer.

`requirements` says what to look for. Without it the server uses the attachment's latest
provisioning receipt, so a domain with nothing attached has nothing to observe; with it, that domain
still reports which records are in place. The set identifies itself by content, so writing the array
inline does not send the mount effect observing in a loop.

<Snippet
  file="examples/react/read-only.tsx"
  region="unattached"
  title="Observing an unattached domain"
/>

A requirement that is not satisfied renders what it expected, what each observer found for that
name, and the observer's own detail line. An observer whose status is `unknown` never answered, so
it reports no values, and host evidence carries none either.

## Abandoning an attempt

A controller's plan, approval, and receipt only mean something for the inputs that produced them, so
changing `domain` or `requirements` drops them. The identity is the content of the requirement
records, not array identity, so writing `requirements={[...]}` inline does not lose a plan the
customer is reading.

Every command carries the key it was raised for and refuses once that key has moved, so a command
raised from a layout effect in the same commit cannot act on the previous domain.

## Failures

<Snippet file="examples/react/controllers.tsx" region="failure" title="Rendering a failure" />

The failed state carries the `DomainKit.Error` itself, so a host reads `reason`, `category`, and
`isRetryable` instead of parsing text. `Outcome.useDescribe()` binds the catalog the root holds and
returns the title and the description for one error; `Messages.failure` joins them into one
sentence.

## Capabilities and permission

<Snippet
  file="examples/react/controllers.tsx"
  region="capabilities"
  title="Rendering only what the server serves"
/>

Capabilities say what the server can serve. `readOnly` says what this customer may do with it, which
no transport can express. A surface of your own reads it through `DomainKit.useReadOnly()`, and
`DomainKit.ReadOnly` narrows one subtree without touching the rest of the page.

<Snippet file="examples/react/read-only.tsx" region="hook" title="Asking which mode a part is in" />

Read-only refuses commands that change the domain, including provisioning and cleanup `retry`.
Connection `retry` re-inspects instead of repeating a write. Verification `observe` and `retry`
stay available. With supplied readiness, both call the host's optional `observe` callback, or do
nothing when it is absent.
