# Server

A template's sign-in page for the demo: the kit's SignIn, against the template's demo session route (`demoSessionRoutes` in ./candor-server.ts). It says plainly that no email is sent and which code to type, and after signing in it goes to `next`. In production the demo is off unless the app sets `CANDOR_DEMO_AUTH=1` (`demoAuthAllowed()` in ./candor-server.ts), and the page says signing in isn't set up.

Replace it with your auth's sign-in page, or keep SignIn and point its `onSendCode` and `onVerifyCode` at your auth.

- Registry name: `server` (`@candor/server`)
- 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/server

## Install

```bash
npx candor-ui add server
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/server`.
It brings in: locale, sign-in, tokens.
npm packages: drizzle-orm@^0.45.4, postgres@^4.0.0.

## <DemoSignIn>

Props:

- `api: string`: The template's session route, such as "/approvals-inbox/api/session".
- `next: string`: Where to go once signed in.
- `brand?: React.ReactNode | undefined`: The product's name or logo, shown above the form.

## Functions

### setDatabase

```ts
setDatabase(db: Database)
```

Use your app's own Drizzle database for every block. Call it once at startup, before the first request.

### database

```ts
database()
```

The shared database: whatever setDatabase() was given, else a pool on DATABASE_URL, opened on first use.

### demoViewer

```ts
demoViewer(request: Request)
```

The demo's viewer: whoever the `candor_demo_user` cookie names. With no cookie it's Riley, so a block works from a clean download; after signing out (the cookie is empty) it's nobody. Not authentication: anyone can set a cookie.

### setViewerSource

```ts
setViewerSource(source: ViewerSource)
```

Replace the demo cookie with your auth. Call it once at startup, before the first request.

### crossSite

```ts
crossSite(request: Request)
```

Whether a page on another site made the browser send `request`. Every route in the blocks and templates that changes something (POST, PUT, PATCH, DELETE) asks this first and answers true with crossSiteRefused(). Safe methods (GET, HEAD, OPTIONS) are never refused.

How it decides. Browsers say where a request came from. `Sec-Fetch-Site` is trusted when it's there: "same-origin", or "none" (the person typed the address), may go on; "same-site" (a sibling subdomain) and "cross-site" may not. Without it, `Origin` must name this app's host (`X-Forwarded-Host` behind a proxy, else `Host`). An origin listed in `CANDOR_TRUSTED_ORIGINS` (comma-separated, e.g. `https://admin.example.com`; compared as origins, so case, a default port and a trailing slash don't matter) may always go on. A request with neither header didn't come from a browser page, and goes on.

What it protects against: cross-site request forgery, a page elsewhere making a signed-in person's browser send a form or a fetch here with their cookies, so that it acts as them.

What it doesn't: a script running on the app's own origin (cross-site scripting); a caller outside a browser, such as curl or another server, which sends whatever headers it likes and is held back only by your auth; a route that changes something on GET; and browsers too old to send either header. It says nothing about who sent the request: that is getViewer().

### crossSiteRefused

```ts
crossSiteRefused()
```

What a route answers, with a 403, when crossSite() says another site's page sent the request.

### demoAuthAllowed

```ts
demoAuthAllowed()
```

Whether the demo cookie may say who is signed in: always outside production, and in production (`NODE_ENV` is "production") only when the app opts in with `CANDOR_DEMO_AUTH=1`. So a production deploy that never called setViewerSource() signs nobody in, rather than every visitor as the demo's first person.

### getViewer

```ts
getViewer(request: Request)
```

Who sent `request`, by your auth; null when nobody is signed in. Until you set a source it's the demo cookie, except in production without `CANDOR_DEMO_AUTH=1`, where it's null for everyone (and the server log says why, once).

### viewerName

```ts
viewerName(request: Request)
```

The signed-in person's name, which the blocks record as who did what; null when nobody is signed in.

### signedOut

```ts
signedOut()
```

What a route answers when nobody is signed in.

### demoSessionRoutes

```ts
demoSessionRoutes(people: DemoPerson[], options?: DemoSessionOptions | undefined)
```

A template's sign-in route for the demo (`api/session`): GET says who is signed in and lists the demo's people, POST `{ email, code }` signs that person in, DELETE signs out. Nothing is sent and nothing is checked beyond the code above; it sets the cookie demoViewer() reads (not HttpOnly, so the blocks' demo "signed in as" switches can still change it). In production without `CANDOR_DEMO_AUTH=1` every method answers 403 with `{ error, demo: false }` and lists nobody. `options.allowed` decides that instead of the environment.

Replace it with your auth's own sign-in (its route, or its hosted page) and call setViewerSource() with how your auth reads a request. The template's pages then need nothing else changed: they ask getViewer() through the API, and a 401 sends the person to the sign-in page.

### errorText

```ts
errorText(error: unknown, sentence = "That couldn't be done because of an error on the server.")
```

What a person is told when something failed, without telling them how. A Refusal was written for them, so its message comes back as it is. Anything else (a database error, a setting that's missing, a bug) is logged on the server with a short reference, and the person gets `sentence` with that reference, which they can quote to whoever reads the log. Use it wherever an error's words would reach the browser: a route's answer (see errorResponse()), an agent stream's `onError`, a tool's result.

### errorResponse

```ts
errorResponse(error: unknown, sentence?: string)
```

A route's answer to an error: a Refusal's own message with its status, or, for anything else, a 500 with `sentence` and a reference, the error itself going to the server log under the same reference (see errorText()).

### guardTools

```ts
guardTools(server: T)
```

For a Claude Agent SDK tool server, `createSdkMcpServer(guardTools({ name, tools }))`: an error a tool throws reaches the model, and the browser that shows the tool's result, as errorText() says it, a Refusal as written and anything else as a fixed sentence with a reference (the detail goes to the server log). Without it the SDK sends any error's message as the tool's result.

### guardToolSet

```ts
guardToolSet(tools: T)
```

For a Vercel AI SDK tool set, `streamText({ tools: guardToolSet({ … }) })`: an error a tool throws reaches the model, and the browser that shows the tool's result, as errorText() says it, a Refusal as written and anything else as a fixed sentence with a reference (the detail goes to the server log). Without it the model reads any error's message, and can repeat it to the person. The tool still fails, so the call shows as failed, and the route's `onError` says the same sentence rather than logging it twice. Tools without `execute` are left as they are; so is an error a streaming tool throws once its first result is out.

### approvalSigning

```ts
approvalSigning()
```

The AI SDK's approval signing, spread into `streamText()`: with CANDOR_APPROVAL_SECRET set, each approval request is signed, so a browser can't approve a call the server never asked about. Without it, nothing is signed.

## Strings (`demoSignIn`)

The fixed words, English by default. Override any of them under `demoSignIn` in `<LocaleProvider strings={{ demoSignIn: { … } }}>`; the table is exported as `demoSignInStrings`.

- `hint`: This is a demo with no real accounts. Sign in as {emails}.
- `codeSent`: This demo sends no email. Type {code} to sign in as {email}.
- `unreachable`: The sign-in couldn't be reached. Check your connection and try again.
- `failed`: That didn't work. Try again.
- `off`: Signing in isn't set up here. The demo sign-in is off in production, and this app hasn't connected its own sign-in yet.

## Types

### Database

Any Drizzle Postgres database, whatever its driver; each block's tables are its own, so the handle has no schema.

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

### Viewer

A person who is signed in, as your auth knows them. `name` is what the blocks record as who did what.

```ts
{ id: string; name: string }
```

### ViewerSource

Your auth's answer to "who sent this request?": the person, or null when nobody is signed in.

```ts
(request: Request) => Viewer | null | Promise<Viewer | null>
```

### DemoPerson

One of the demo's people, as its sign-in page lists them.

```ts
{ name: string; email: string }
```

### DemoSessionOptions

How demoSessionRoutes() decides whether it answers.

```ts
{
  /**
   * Whether the demo sign-in answers, decided by the host rather than read from `process.env` on each request: true
   * where the demo is the point and there's no environment to read (a demo running in the browser), false to turn it
   * off. Defaults to demoAuthAllowed().
   */
  allowed?: boolean | undefined;
}
```

## 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`.
