Manifest
Every app ships a block.manifest.json that declares its identity, the scopes it needs, and how it renders. The platform publishes the canonical JSON Schema (Draft 2020-12) for this file at https://civitai.com/schemas/app-block/v1.json. Set that URL as your manifest's $schema for editor validation. The table below is generated from the same canonical schema — the one the @civitai/app-sdk and the civitai CLI vendor and validate against — so it never drifts from what the server accepts.
| Field | Type | Required | Notes |
|---|---|---|---|
$schema | string | optional | Optional JSON-Schema reference; ignored by the platform validator. |
blockId | string | required | The block slug. Becomes the canonical submission slug. Lowercase, starts with a letter, hyphen-separated, 3-40 chars. pattern: ^[a-z][a-z0-9-]*[a-z0-9]$, minLength 3, maxLength 40 |
version | string | required | Semantic version (x.y.z, optional -prerelease). pattern: ^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$, minLength 1 |
name | string | required | Human-readable display name. The server only requires a non-empty string (no length cap), so the CLI does not impose one either. minLength 1 |
type | "block" | optional | Free-form descriptor used by some examples (e.g. "block"). Not validated by the platform. |
contentRating | "g" | "pg" | "pg13" | "r" | "x" | required | Content rating of the app surface. |
category | "generation" | "games" | "utility" | "discovery" | "moderation" | "analytics" | "other" | optional | Optional marketplace category for the app's `/apps` store listing. When present it flows to the listing automatically on moderator-approve (only when a moderator has not already curated a category). Omit to let a moderator categorise the app. Must be one of the known marketplace categories — this enum is kept in lockstep with MARKETPLACE_CATEGORIES in src/server/services/blocks/marketplace-categories.constants.ts (a drift-guard test enforces equality). |
renderMode | "iframe" | "inline" | "hybrid" | optional | How the block renders. Defaults to "iframe". "inline"/"hybrid" require a verified/internal trust tier (server-assigned), so authors should leave this as iframe. |
trustTier | "unverified" | "verified" | "internal" | optional | SERVER-OWNED. Do NOT set this in your manifest — the platform assigns the trust tier during review. Present here only to reject dev-set values. |
scopes | string[] | required | Capabilities the block requests. Must be a strict subset of what review grants. Each scope is lowercase colon-separated. |
minApiVersion | string | optional | Minimum App SDK API version the block targets (informational). pattern: ^\d+(\.\d+)*$ |
buildCommand | string | optional | Config-as-code: command the platform runs to build the static bundle (e.g. "npm run build"). Omit for no-build (static) apps. When set, outputDir must also be set. minLength 1, maxLength 256 |
outputDir | string | optional | Config-as-code: directory (relative to the project root) the buildCommand emits static files into (e.g. "dist"). Required when buildCommand is set. pattern: ^[^/].*, minLength 1, maxLength 256 |
publicSettingsKeys | string[] | optional | Allowlist of settings keys exposed to anonymous viewers. Default (omitted) = none exposed. maxItems 32 |
assetBundleUrl | string (uri) | optional | Optional v2 surface — HTTPS URL to a hosted asset bundle. Must be a public https URL. pattern: ^https:// |
iframe | object | optional | iframe envelope. NOTE: iframe.src is SERVER-OWNED — do NOT set it; the platform stamps the canonical bundle URL during build/approve. |
page | object | optional | Full-page surface descriptor (W10). Page apps mount at /apps/run/<slug>. |
targets | object[] | optional | Model-page slot targets. Each target's slotId must be a known registered model slot (not the page slot). Optional for page-only apps. maxItems 16 |
Required fields
blockId, version, name, contentRating, and scopes are always required. iframe.src is server-owned — do not set it (see below); the platform stamps the canonical bundle URL during build/approve.
Note the tightened constraints the schema now surfaces (all server-enforced):
blockId— a DNS-label slug: lowercase, starts with a letter, 3–40 chars,^[a-z][a-z0-9-]*[a-z0-9]$. It becomes<blockId>.civit.ai.version— semantic version (x.y.z, optional-prerelease), not just any non-empty string.scopes— each entry must be one of the known scopes (the enum in the table above), not merely a well-formeda:b:cstring. See Scopes.
Optional fields worth calling out
category(enforced) — an optional marketplace category for the app's/appsstore listing. If present it must be one of the enum values in the table; an unknown value is rejected at submit time. Omit it to let a moderator categorise the app.assetBundleUrl(enforced) — an optional v2 surface. Must be a publichttps://URL on an origin registered in your app's OAuth-clientallowedOrigins(SSRF + origin binding); private, non-HTTPS, or off-origin values are rejected.typeandminApiVersion(informational) — accepted but not enforced by the validator. Safe to include as documentation; don't treat them as load-bearing.
Server-owned fields
Some fields appear in the schema for completeness but are owned by the platform — a value you submit is normalized or overridden server-side:
iframe.src— normalized and host-allowlisted at registration. You don't point this at your own host; the platform serves your app fromhttps://<slug>.civit.ai/.trustTier— always assigned by the server; a submitted value is ignored.
What the schema can't express (the validator wins)
The JSON Schema describes the manifest's shape and enums. The authoritative BlockManifestValidator at submit time additionally enforces semantic rules that JSON Schema can't:
- SSRF host allowlisting on
iframe.src. - Scope ⊆ OAuth-client — your declared
scopesmust be a subset of the app's OAuth-client allowed bits (see Scopes). - Sandbox-token allowlisting by trust tier.
buildCommandallowlist — only a fixed set of build invocations is permitted. The published schema only bounds the length; the validator carries the positive allowlist regex and additionally rejects shell metacharacters. AbuildCommandthat passes local schema validation can still be rejected at submit time.outputDirtraversal — the schema blocks a leading/; the validator also rejects.., backslashes, and other traversal/escape sequences.
In two small spots the published schema is marginally stricter than the runtime validator: it locks type to ["block"] and requires outputDir whenever buildCommand is set, whereas the validator ignores type and defaults outputDir. The server validator is the true gate — and it enforces more than the schema elsewhere (the semantic rules above) — so don't over-constrain based on the table alone.
The validator is the enforcement boundary
A manifest can pass local JSON-Schema validation and still be rejected at submit time. If the schema and the validator ever conflict, the validator wins.