1.0 product boundary
Status: Pre-MVP. Not released. Last updated: 2026-08-27.
Basis: current repository code, focused local tests and the canonical specifications. No complete real-Cloudflare acceptance ledger or authorized four-site portfolio acceptance exists. SPEC.md §11 owns release gates.
Product: a headless CMS for defining and operating structured content that can generate an adopter-owned standalone backend. Graduation emits a normalized Prisma schema, reviewable migrations, generated routes with familiar Prisma model/query semantics and published content into a repository, database and runtime the adopter owns. The generated backend reads normalized tables and has no runtime dependency on Dee Wan.
Cloudflare is the current platform. Strapi is the first migration adapter. Neither defines the audience. Import helps adoption and portability; it is not the product thesis.
Each capability is in one list: 1.0 contract, supervised pilot, or unavailable. “Unavailable” is not a promise to build it.
1.0 contract — built and locally verified
Section titled “1.0 contract — built and locally verified”Content operations
Section titled “Content operations”- UI-defined content models and fields: text, rich text, number, boolean, select, date, media, JSON, relation and component.
- Breaking-change checks before a model definition is applied.
- Multilingual content with configurable languages and locale fallback policy.
- Immutable published snapshots, version history, restore and publish/unpublish.
- Direct draft/publish and configurable approval-workflow machinery.
- Optional review mode, off by default and per site. The General settings tab carries the toggle;
PATCH /api/sites/:idrefuses to turn it off while content sits in a review state, and names the items. With it on, the dashboard is the review queue — what is waiting, what this caller may do next, what a row they cannot move is waiting on, and separately what was sent back. With it off, the dashboard shows what changed lately. No workflow definition is rewritten either way, so a site that customised one gets it back unchanged. - Unsaved-work recovery for ordinary content editing AND the page composer. An edit is written to browser-local storage against the server baseline it was taken from, and offered back only against that same baseline, so a restore can never silently overwrite somebody else’s save. Covers a crashed tab, a reload and a reclaimed process; the in-app leave guard covers a deliberate navigation. In the composer the baseline is the page’s current version id, and the kept work includes an UNAPPLIED PROPOSAL — the one thing on that screen that cost a provider call and that the product deliberately writes no version for. Browser-local and per-device: it is not server-backed cross-device draft continuity.
- Site-owned components, with an optional shared library.
component.site_idset means one site owns it andmodel:applyon that site is enough; NULL means the shared library, which every site reads and only a superadmin writes. Uniqueness is per owner, so two sites may each own ahero. A site may FORK a shared component into itself. A fork is a SITE-WIDE OVERRIDE: it rewrites no row, and every field on that site already naming the slug resolves to the fork afterwards. The route returns the count of references it re-aimed and the UI states the consequence before the click. An ordinary create that would land on a shared slug is REFUSED until the caller acknowledges the override. A component’s SLUG IS IMMUTABLE after creation — it is how every field, nested component and preset refers to it and nothing stores an id, so a rename would silently re-point or orphan every reference; the name stays editable. Fork warnings and the delete guard count references from BOTH model fields and other component definitions. Components are referenced BY SLUG throughout, so an owned component SHADOWS the shared one of the same slug — a site’s visible set has unique slugs, and a shadowed reference does not count against the shared component when it is deleted. Applying imported component definitions is unblocked at the schema and authorization level; the importer slice that writes them is NOT built and component fidelity is not demonstrated. - Per-site roles and owner-restricted permissions.
- Page composition into a framework-neutral structured document. Revisions use normal versioning, publishing rules and rollback. This is a content contract, not a website renderer.
Core detachable backend delivery
Section titled “Core detachable backend delivery”- Authenticated
GET /api/models/public-client.tsemits a dependency-free TypeScript client for one site: exact model fields, locale union, query types, response envelope and model methods. - The generated client is regenerated manually after a model change. It has no automatic drift detection or artifact versioning.
- Graduation emits an adopter-owned typed backend and a snapshot of content published at generation time. The generated runtime is detached from Dee Wan and uses its own bearer token.
- The CMS-managed regeneration workflow is coordinated: it pulls definitions and reports its result to Dee Wan. Runtime authority and build coordination are separate.
Tenancy and identity
Section titled “Tenancy and identity”- Multiple sites in one installation with explicit site scope. Callers cannot infer a tenant or choose a database binding.
- Logical isolation in the shared bootstrap database.
- Better Auth database sessions, bootstrap claim, password ownership, recovery, revocation, lockout and request limits.
- Optional Cloudflare Access perimeter, off by default.
- Existing accounts can receive per-site grants. Invitations are unavailable.
Public reading
Section titled “Public reading”- Separate public-read Worker. Site resolution uses
Host, never a caller-provided site ID. - Published snapshot reads, strict or fallback locale policy, cursor pagination, relation/tag filters and cache-tag purge on publish.
- Public models need no token. Private models use site-scoped read tokens and do not enter shared cache.
- The Worker’s D1 binding is read-only by code-review rule. D1 has no read-only binding setting.
Media and AI
Section titled “Media and AI”- R2 or Cloudflare Images storage, with WebP/AVIF conversion.
- Text generation, translation and image generation through a server-side DeepInfra proxy. Provider keys do not reach the browser.
- Per-site hourly quota. No additional inference provider is supported.
Portability and migration adapters
Section titled “Portability and migration adapters”- A portable Dee Wan site bundle and CSV/XLSX content transfer.
- First external adapter: Strapi v4/v5 archive inspection, explicit partition mapping, dry run, resume, deterministic rerun, receipts, problem rows, manifests and guarded undo.
- Ambiguous source ownership blocks the dry run. The importer does not guess from names, slugs or URLs.
- Real portfolio fixtures, deployed Worker limits and cutover/rollback proof remain acceptance work.
Operations
Section titled “Operations”-
CLI install planning, apply/resume, diagnostics, verification, manifest and dry-run teardown.
-
Local browser installer for a developer who already has Node, a checkout and a terminal.
-
Deployment history and rollback refusal after a destructive migration.
-
Outbound webhook configuration, signed delivery history, secret rotation and manual replay.
-
Error reports. Unhandled errors go to the Dee Wan Sentry project with content, form values, emails and credentials removed first. A superadmin turns them off for the whole deployment at Settings → Integrations → Error reports; the Worker variable
DEEWAN_ERROR_REPORTING=offdoes the same.
-
Bounded automatic retry for outbound webhooks. A delivery that failed for a reason that could pass later — no answer,
408,429, any5xx— is retried on an exponential, jittered schedule up to five attempts, honouringRetry-After; a refusal the subscriber would repeat is terminal at once. Every attempt carries the same event id, so a duplicate caused by a timeout is one a consumer can recognise. Pending attempts are exempt from the delivery-history cap, and a retry sends the exact recorded bytes signed with the subscriber’s CURRENT secret. Local coverage only; no deployed evidence. -
Rich text is trusted-author HTML, and a RENDERER MUST SANITISE IT. Its generated type is
Html— neverSafeHtml, never genericJson— because nothing in this product sanitises or rewrites stored markup. Two things ARE enforced at the single write boundary: the value must be a STRING (a non-string is a structured 400 naming the field and the type it received, never a stringified[object Object]), and it must be at most 256 KiB of UTF-8, counted in bytes. Anything a permitted author or an AI generation produced is published as authored, so a consumer that inserts it into a page without sanitising is the one taking the risk. -
One Delivery area (Settings → Delivery). The live CMS read API and the graduated backend are presented as two DIFFERENT contracts, with the route, authentication, envelope, deployment, rollback and CMS-dependency differences stated before a cutover. It reports never generated, current, drifted, building, build/deployment failure and detached, and the revision list keeps its own per-revision rollback refusal. Local coverage only; no deployed evidence.
-
Generated-artifact version and contract drift. Artifact identity is the generator version plus a deterministic digest of the input contract — never a timestamp — so an unchanged site rebuilt by a changed generator is reported as drifted rather than current. A deployment that recorded no generator version reads as drifted, not as agreement. Regeneration stays explicit: nothing regenerates or deploys on its own.
-
An administrative audit ledger. Who changed a model, a component, a role, a grant, a site token, settings, infrastructure, the hostname, or who deployed and rolled back — recorded in the control plane, partitioned by site, as an intent written BEFORE the action and a result written after, joined by one operation id. The action does not run if its intent cannot be written; if the result cannot be written the intent is kept and the outcome reads as UNKNOWN rather than as success. One read behind its own
audit:readpermission, at Settings → History. Append-only: no write route exists. It is not telemetry — no counters, no aggregates, no dashboard.
Supervised pilot — built locally, external proof incomplete
Section titled “Supervised pilot — built locally, external proof incomplete”- Per-site content D1. One control D1 plus one content D1 per site is the selected topology. Schema split, routing, provisioning, migration fan-out, backup/restore and teardown are locally verified. Real Cloudflare, capacity, backup/restore and four-site isolation evidence is absent.
- CMS-driven graduation deployment. The claim, build token, generated repository workflow, migration lineage, report-back and rollback controls are locally verified. No retained GitHub Actions plus Cloudflare deployment proves the whole loop.
- Complete installer path. The admin Worker and UI follow the normal setup path.
setupalso WRITESbackend/wrangler.public.jsonc, at the point where the account, the zone and the database id are all settled; it never overwrites an existing one, and it REFUSES BY NAME rather than guessing a value it cannot derive. Manual preparation from the example is the fallback for that refusal, not the normal path. Without the file,deployREFUSES rather than skipping: an install that deliberately serves no public content says so with--admin-only, and the run is then reported as admin-only rather than as complete. There is no silent skip and no fallback to another install’s public config. What remains open is deployed evidence, not the file. - Preview routing. The API stores an HTTPS renderer origin and per-model route templates, and the editor opens the returned rendered URL. Settings → Preview reads and writes that contract, states what an unconfigured site’s Preview button does, and names each refusal. Without a configured renderer, Preview opens the public-API JSON response, and the screen says so.
- Remote plugin protocol v1 (core side). Installation from an HTTPS manifest with a hand-chosen
capability and model grant, hashed and rotatable installation tokens, disable/enable/uninstall,
one-time delegated launch, scoped content reads and the pinned publish/unpublish command
(
SPEC-PLUGINS.md). The command re-reads the delegating person’s authority at execution, applies at most once per idempotency key and workflow revision, publishes the whole translation group and answers every refusal with a named terminal code. Local coverage only; no deployed evidence. - Backup and restore. Built and verified against local SQLite. Not verified against real D1.
- Four-site Strapi migration. The generic adapter path exists. Authorized archives, consumer contracts, reviewed partitions, staging, cutover and rollback evidence do not.
Later / unavailable — not built or disabled
Section titled “Later / unavailable — not built or disabled”- A production reference renderer, starter theme and complete SEO/sitemap/redirect delivery path.
- A zero-terminal installer or registered Cloudflare OAuth application.
- A GitHub App; current repository wiring uses device flow.
- Invitations, entirely. The dormant link mechanism is behind
ENABLE_INVITATIONS; tracked configurations leave it unset and all invitation routes answer 404. No verified mail delivery or address-proof step exists. - Bulk actions, saved views and coordinated content releases.
- Media crop, focal point and duplicate handling. Media SEARCH and alt-text filtering are built.
- Review assignments, comments and notifications.
- Webhook queueing beyond the bounded retry above, and any delivery guarantee stronger than at-least-once.
- Continuous CMS-to-graduated-database synchronization.
- Exact-copy or locked-section editing in the page composer.
- Model-level database sharding.
- Enterprise SSO, MFA and passkeys.
- Scheduled publish and unpublish in core. Scheduling is the separate Scheduling plugin (
plugins/scheduling/), not deployed.
Delivery truth
Section titled “Delivery truth”Dee Wan has two developer delivery surfaces:
- The CMS public API is live. Publishing updates its snapshot reads immediately and purges cache tags.
- A graduated backend is a schema-and-content release. It contains what was published when the generation ran. Later CMS publishes reach it only after explicit re-graduation and redeployment.
Do not describe graduation as a live feed. Do not describe local test coverage as a release. The first 1.0 claim requires every open external and final gate in SPEC.md §11 to pass on one exact commit.