# Admin configuration reference

> The `AdminConfig` schema and its defaults: `signupsEnabled`, `siteName`, `instanceInstructions`, `announcement`, `banner`, `accentColor`, `disabledResources`, `disabledGatekeepers`, `ambientGatekeeperModes`, and `formats`. Covers length limits, the `AdminSettings` DO as sole writer mirroring to the reserved `.adminConfig` KV key, `readAdminConfig` hot-path reads, and why auth config is deliberately excluded.

- Repository: cloudflare/cloudflare-os
- GitHub: https://github.com/cloudflare/cloudflare-os
- Human docs: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd
- Complete Markdown: https://grok-wiki.com/public/docs/cloudflare-cloudflare-os-838773bb92dd/llms-full.txt

## Source Files

- `packages/workshop-backend/src/admin-config.ts`
- `packages/workshop-backend/src/admin-settings.ts`
- `packages/workshop-backend/src/provisioning-policy.ts`
- `packages/workshop-shared/src/api.ts`
- `packages/workshop-backend/src/user.ts`
- `AGENTS.md`

---

---
title: "Admin configuration reference"
description: "The `AdminConfig` schema and its defaults: `signupsEnabled`, `siteName`, `instanceInstructions`, `announcement`, `banner`, `accentColor`, `disabledResources`, `disabledGatekeepers`, `ambientGatekeeperModes`, and `formats`. Covers length limits, the `AdminSettings` DO as sole writer mirroring to the reserved `.adminConfig` KV key, `readAdminConfig` hot-path reads, and why auth config is deliberately excluded."
---

`AdminConfig` is a single deployment-wide object declared in `packages/workshop-backend/src/admin-config.ts`, owned by the `AdminSettings` Durable Object and mirrored into one reserved `BLUEPRINTS` KV key (`ADMIN_CONFIG_KEY`). It carries only the "soft" deployment customizations — branding, agent instructions, and which gatekeeper connectors/resources and output formats are offered. Every field is enabled by default; the admin UI opts things *out*. Hot paths (`getServerConfig()` on each (re)connect, and the agent) resolve it with a single cheap KV get via `readAdminConfig` rather than touching the singleton DO.

## Schema

```ts
// packages/workshop-backend/src/admin-config.ts
export type AdminConfig = {
  signupsEnabled: boolean;
  siteName: string;
  siteLogoConfigured: boolean;
  instanceInstructions: string;
  announcement: string;
  banner: BannerConfig;
  accentColor: string;
  disabledResources: Record<string, string[]>;
  disabledGatekeepers: string[];
  ambientGatekeeperModes: Record<string, AmbientGatekeeperMode>;
  formats: FormatCuration[];
};
```

<ParamField body="signupsEnabled" type="boolean" required>
Whether new account signups are allowed. An access toggle only — which auth providers exist and whether password login is on stay env-driven.
</ParamField>

<ParamField body="siteName" type="string" required>
Site name shown next to the top-bar logo, or `""` to fall back to `DEFAULT_SITE_NAME`. Resolve for display with `resolveSiteName()`. Bounded by `MAX_SITE_NAME_LENGTH`.
</ParamField>

<ParamField body="siteLogoConfigured" type="boolean" required>
Whether this deployment has a custom site logo. The image bytes are stored separately (R2, `SITE_LOGO_R2_KEY`), not in the config object.
</ParamField>

<ParamField body="instanceInstructions" type="string" required>
Extra instructions appended to the agent system prompt. Bounded by `MAX_INSTANCE_INSTRUCTIONS_LENGTH`.
</ParamField>

<ParamField body="announcement" type="string" required>
Centered top-bar notice, rendered as Markdown. Bounded by `MAX_ANNOUNCEMENT_LENGTH`.
</ParamField>

<ParamField body="banner" type="BannerConfig" required>
Full-width banner: `{ text, color }`. Colors are validated with `isBannerColor`; the default is `DEFAULT_BANNER_COLOR`.
</ParamField>

<ParamField body="accentColor" type="string" required>
Accent (brand) color as a hex string, validated with `isHexColor`, or `""` for the default theme.
</ParamField>

<ParamField body="disabledResources" type="Record<string, string[]>" required>
Disabled gatekeeper resources, keyed by `vendorId` to the list of disabled resource `urlPattern`s. Read back through `isResourceDisabled` / `filterEnabledResources`.
</ParamField>

<ParamField body="disabledGatekeepers" type="string[]" required>
Vendor ids that are fully disabled for the deployment.
</ParamField>

<ParamField body="ambientGatekeeperModes" type="Record<string, AmbientGatekeeperMode>" required>
Per-vendor provisioning mode for auto-provisioning ("ambient") gatekeepers. Keys are lowercased vendor ids. An absent entry means the default. Only meaningful for vendors declaring `VendorDescription.autoProvisionsAccount`.
</ParamField>

<ParamField body="formats" type="FormatCuration[]" required>
The blueprints offered as this deployment's standard output formats — what a user gets from "New Slides", and what the agent is told to prefer. Array order is menu order.
</ParamField>

### Defaults

`DEFAULT_ADMIN_CONFIG` is the value the `adminConfig` storage singleton starts at, so a deployment that has never been configured behaves as fully permissive:

| Field | Default |
|---|---|
| `signupsEnabled` | `true` |
| `siteName` | `""` |
| `siteLogoConfigured` | `false` |
| `instanceInstructions` | `""` |
| `announcement` | `""` |
| `banner` | `{ text: "", color: DEFAULT_BANNER_COLOR }` |
| `accentColor` | `""` |
| `disabledResources` | `{}` |
| `disabledGatekeepers` | `[]` |
| `ambientGatekeeperModes` | `{}` |
| `formats` | `[]` |

### Length limits

| Constant | Applies to | Defined in |
|---|---|---|
| `MAX_SITE_NAME_LENGTH` | `siteName` | `@gadgets/workshop-shared/api` |
| `MAX_ANNOUNCEMENT_LENGTH` | `announcement` | `@gadgets/workshop-shared/api` |
| `MAX_INSTANCE_INSTRUCTIONS_LENGTH` | `instanceInstructions` | `@gadgets/workshop-shared/api` |
| `MAX_AGENT_HINT` (`400`) | `FormatCuration.agentHint` | `admin-config.ts` |

<Note>
`MAX_AGENT_HINT` is a prompt budget, not a validation limit: every enabled format's `agentHint` goes into the system prompt on every turn. `parseFormats` trims and truncates with `.slice(0, MAX_AGENT_HINT)` rather than rejecting the entry.
</Note>

## Format curation

```ts
export type FormatCuration = {
  blueprintId: string;
  enabled: boolean;
  agentHint?: string;
  overrides?: Partial<BlueprintOutput>;
};
```

The blueprint itself supplies the noun, plural, and icon, so improving a blueprint improves every deployment that has not overridden it. `overrides` substitutes the deployment's own presentation (e.g. an org that calls its decks "Briefings"); absent fields fall back to the blueprint's declaration and are cleaned by `sanitizeOutputOverrides` (which returns `undefined` when no field survives).

Disabling a format keeps the entry and its overrides around, so re-enabling does not lose an admin's edits. `formats` is separate from a blueprint's own `BlueprintMetadata.output` declaration: any user can publish a blueprint calling itself a Document, but only this list decides what the deployment offers.

### Parsing and reordering

- `parseFormats(value)` accepts only well-formed entries: non-object entries, entries without a non-empty string `blueprintId`, and duplicate ids are skipped; `enabled` defaults to `true` unless it is exactly `false`.
- `reorderFormats(formats, blueprintIds)` returns `formats` rearranged into the given order and throws `"Format order must list each promoted format exactly once."` unless `blueprintIds` is a permutation of what is promoted. Uniqueness is checked separately from length because the lookup `Map` dedupes — `[A, A]` against promoted `[A, B]` would otherwise pass both a length and a membership test, drop `B`, and leave a duplicate that makes every later reorder throw.
- `defaultOutputFormatId(blueprintId)` gives a stable grouping id for a promoted blueprint that declares no output: ids of 40 characters or fewer pass through unchanged, longer ones become `` `${blueprintId.slice(0, 31)}-${fingerprint(blueprintId)}` ``.
- `fingerprint(text)` is FNV-1a rendered as eight hex characters — synchronous (so callers need not be async, unlike `crypto.subtle.digest()`) and compared only for equality, so nothing depends on collision resistance.

## Ambient gatekeeper modes

`provisioning-policy.ts` is the single chokepoint for resolving `ambientGatekeeperModes`. `UserDurableObject` reads `AdminConfig` and calls these helpers when provisioning, listing, and surfacing ambient accounts.

| Mode | Behavior |
|---|---|
| `disabled` | Not available; no account is provisioned, and any existing one stays dormant. |
| `optional` | **Default.** Users opt in from the Connectors page; not forced on anyone. |
| `enabled` | Auto-provisioned for every user (forced); hidden from the Connectors list and not user-removable. |

```ts
// packages/workshop-backend/src/provisioning-policy.ts
export const DEFAULT_AMBIENT_GATEKEEPER_MODE: AmbientGatekeeperMode = "optional";

export function ambientGatekeeperMode(config: AdminConfig, vendorId: string): AmbientGatekeeperMode {
  return config.ambientGatekeeperModes?.[vendorId.toLowerCase()] ?? DEFAULT_AMBIENT_GATEKEEPER_MODE;
}

export function shouldAutoProvisionAccount(config: AdminConfig, vendorId: string): boolean {
  return ambientGatekeeperMode(config, vendorId) === "enabled";
}
```

`ambientGatekeeperMode` uses optional chaining on `ambientGatekeeperModes` deliberately: it tolerates a config persisted before that field existed. The default is `optional` because ambient authority is not imposed on every user unless an admin explicitly turns it on — consistent with the kernel rule that a resource becomes ambient only by user/admin configuration and never by a gatekeeper asserting its own ambience.

## Storage: one writer, one KV mirror

```mermaid
flowchart LR
  subgraph admin["Admin surface"]
    api["AdminApi (Cap'n Web)<br/>admin-settings.ts"]
  end

  subgraph do["AdminSettings DO — getByName(\"\")"]
    single["typed-storage singletons:<br/>adminConfig / installedFormatBlueprints /<br/>promotedFormatBlueprints"]
    coll["collection featuredBlueprints"]
    tail["adminConfigMutationTail<br/>siteLogoMutationTail"]
  end

  subgraph stores["Shared stores"]
    kv["BLUEPRINTS KV<br/>ADMIN_CONFIG_KEY (.adminConfig)<br/>FEATURED_BLUEPRINTS_KEY"]
    r2["R2 — SITE_LOGO_R2_KEY"]
  end

  subgraph hot["Hot paths (read-only)"]
    cfg["getServerConfig() / login"]
    user["UserDurableObject<br/>provisioning + resource listing"]
    agent["agent system prompt"]
  end

  api --> tail --> single
  single -->|serializeAdminConfig| kv
  api --> r2
  coll --> kv
  kv -->|readAdminConfig| cfg
  kv -->|readAdminConfig| user
  kv -->|readAdminConfig| agent
```

The `AdminSettings` Durable Object is always addressed as `getByName("")`. It holds settings only admins may modify, and it publishes them to KV so ordinary user requests never touch the singleton DO (which they could otherwise overload). Having exactly one DO write the KV key also avoids races when updating it.

Within the DO, every config setter writes the same authoritative `adminConfig` singleton plus its KV mirror, so the full read/modify/write is serialized on `adminConfigMutationTail` — otherwise external KV I/O could let concurrent setters lose updates. Site logo changes are serialized separately on `siteLogoMutationTail`, because R2 and the config are distinct stores and reset/upload must not interleave while switching whether the fixed public object is enabled.

<Warning>
`ADMIN_CONFIG_KEY` (`.adminConfig`) and `FEATURED_BLUEPRINTS_KEY` are reserved keys in the `BLUEPRINTS` namespace, screened by `isReservedBlueprintKey`. Do not write them from anywhere but `AdminSettings`, and do not treat them as blueprint records.
</Warning>

### Reading on the hot path

`readAdminConfig` (exported from `admin-config.ts`, taking a `BlueprintKvEnv`) is the read entry point; consumers import it rather than reaching for the DO. `user.ts` imports `readAdminConfig`, `filterEnabledResources`, and `isResourceDisabled` together, resolving admin policy inside `UserDurableObject` with a single KV get.

## Bundled format installation and one-shot promotion

`AdminSettings.ensureFormatBlueprintsInstalled()` installs the format blueprints bundled with the deployment. It is idempotent and cheap — an up-to-date deployment does one string comparison against the `installedFormatBlueprints` singleton (`formatBlueprintsManifestVersion()`) and returns. Callers are coalesced onto one in-flight run (`#installInFlight`), because two isolates racing on a fresh deployment would otherwise both promote the same blueprints, and a duplicated id makes `setFormatOrder()` reject every reordering.

<Steps>
<Step title="Compare the manifest stamp">
If `installedFormatBlueprints` already equals `formatBlueprintsManifestVersion()`, skip installation.
</Step>
<Step title="Install and mirror featured records">
`installFormatBlueprints(env)` returns the installed `BlueprintPublicInfo` records. They are written straight into the `featuredBlueprints` mirror rather than through `setBlueprintFeatured()`, whose authoritative bit lives in the publishing user's DO — these have no owning user. Then `#writeFeaturedSnapshot()` publishes the snapshot.
</Step>
<Step title="Stamp only on a complete install">
`complete` is `installed.length === FORMAT_BLUEPRINTS.length`. The stamp is written only when complete, so a crash or one bad archive retries next time; recording a partial install as complete would strand the failed entries until the manifest changed again. A partial install resolves `false` instead of throwing — the caller has nothing to handle but does need to know to ask again.
</Step>
<Step title="Check promotion every run">
`#promoteBundledFormats()` runs on every pass, not just after an install, so a deployment that installed before curation existed still ends up offering its bundled formats.
</Step>
</Steps>

Promotion is tracked in the separate `promotedFormatBlueprints` singleton so each blueprint is offered into `AdminConfig.formats` exactly once, ever. Re-deriving the list from the manifest would undo an admin's removal on every startup; tracking it separately means an admin who removes a format keeps it removed, while an updated archive still refreshes the blueprint without resetting the deployment's curation.

## Why auth config is excluded

Authentication and authorization configuration — which sign-in providers exist, whether password login is on — is deliberately **not** in `AdminConfig`. It stays environment-variable driven (see `auth/config.ts`) so it cannot be changed by a compromised admin session. `signupsEnabled` sits in `AdminConfig` only because it is an access toggle, not authentication config.

<Info>
Because the config is opt-out, the safe posture for a public deployment is not "leave the defaults" — the defaults offer every connector and resource and allow signups. Set `signupsEnabled`, `disabledGatekeepers`, `disabledResources`, and `ambientGatekeeperModes` explicitly.
</Info>

## Related pages

<CardGroup cols={2}>
<Card title="Environment variables" href="/environment-variables">
The env-var surface that owns auth and billing config, including `ADMINS`, `AUTH_GATEKEEPERS`, and `DISABLE_PASSWORD_AUTH`.
</Card>
<Card title="Configure sign-in and AI Gateway billing" href="/configure-signin-and-billing">
Why sign-in posture is env-driven and how allowlisting and identity keying work.
</Card>
<Card title="RPC API reference" href="/rpc-api-reference">
`AdminApi` and the `ServerConfig` shape returned to clients at boot.
</Card>
<Card title="Manage bundled format blueprints" href="/bundled-format-blueprints">
The `.gadget` + `.json` sidecar split, `FORMAT_BLUEPRINTS_DIR`, and first-request installation.
</Card>
<Card title="Gatekeeper protocol" href="/gatekeeper-protocol">
`VendorDescription.autoProvisionsAccount`, resource URL-pattern matching, and ambient mode resolution.
</Card>
<Card title="Blueprints" href="/blueprints">
Blueprint ids, `BlueprintOutput`, and the propagation path behind promoted formats.
</Card>
</CardGroup>
