Skip to content
Dee Wan 1.0 is not yet released (pre-MVP). See the 1.0 product boundary.

Standalone backend: Implementation details

How a generated standalone backend is built: the files it contains, its dependencies, the Cloudflare resources it uses, how content maps to tables, the exported managed renderer and the response cache. For routes, authentication and deployment, see the API reference.

The tree is abridged. It shows the main files, not every emitted file.

.
├── package.json
├── .nvmrc / .npmrc
├── wrangler.jsonc / wrangler.example.jsonc / wrangler.build.jsonc
├── graduation.manifest.json / graduation.generator.json
├── .dee-wan-files.json / .dee-wan-customization.json
├── prisma/schema.prisma
├── migrations/
├── access/read-tokens.json
├── data/published-snapshot.json
│ (data/media-manifest.json after a media transfer)
├── src/
│ ├── app.ts / worker.ts / access.ts / access-policy.ts / auth.ts / security.ts / db.ts
│ ├── access-config-rules.js / access-config-rules.d.ts / credential-digest.ts
│ ├── response-cache.ts / query-manifest.ts / trusted-routes.ts / relation-routes.ts
│ ├── sqlite-batch.ts / d1-column-types.ts / d1-schema-columns.ts
│ ├── backend-customization.ts / backend-config.ts / customization-runtime.ts
│ ├── local.ts / serve.ts / serve-cli.ts / migrate.ts
│ ├── generated/prisma/ / generated/guard/ / generated/routes/
│ └── owned/backend.ts / owned/routes.ts
├── scripts/
│ ├── dee-wan-generator.mjs / regenerate.mjs / check-customization.mjs
│ ├── install-dependencies.mjs / emit-migration.mjs / portable-client.mjs
│ ├── load-snapshot.mjs / configure-deployment.mjs / file-manifest.mjs
│ ├── check-relation-ids.mjs / deploy-from-repository.mjs
│ └── verify-deployment.mjs / verify-media.mjs
├── .github/workflows/deploy-from-repository.yml
│ (site artifacts additionally carry dee-wan-build.yml, the managed
│ build contract; a `--model` artifact has no CMS-managed site build)
├── managed-renderer/
│ (only when the site had an active managed renderer release)
│ ├── package.json / package-lock.json / astro.config.mjs / tsconfig.json
│ ├── wrangler.jsonc / public/ / worker/ / src/
│ └── release/release.json / template.json / route-map.json / site-chrome.json
│ / managed-renderer.mjs / evidence/
└── apps/site/
├── package.json / wrangler.jsonc / astro.config.mjs / public-reads.json
├── public/.assetsignore
└── src/
├── lib/ / pages/ / components/
├── lib/image-endpoint.ts
├── owned/
└── pages/owned/

generated files are fingerprinted and regenerated. The next regeneration rejects edited generated files. owned files are scaffolded only when absent and preserved byte-for-byte. shared files are preserved, with an additive merge for package manifests. Migration history is append-only. The customization guide gives the precise path matrix.

New package names are graduated-site- plus the UTF-8 site ID encoded as lowercase hexadecimal. They remain stable when models are reordered, added or removed. Existing adopter package names and README files are preserved.

Hono handles HTTP. Prisma uses its query compiler and D1 adapter in the API Worker. prisma-generator-express emits the Hono CRUD pipeline and OpenAPI, and prisma-guard enforces query shapes. Local serving uses the better-sqlite3 adapter. eav-to-prisma converts accepted CMS models during generation. The generator is also carried as a digest-pinned build script. It is not a runtime CMS service.

The following inventory is checked against ARTIFACT_TOOLCHAIN_PACKAGES, SITE_MANIFEST, and the installer constants. Root versions come from the actual generating installation and graduation.manifest.json.toolchain. Installed versions are taken from these sources. Every root entry is an exact pin. The site accepts the listed ranges and resolves them in its own lockfile.

Install root Package Package block Version authority
root @dee-wan/cache dependencies toolchain.responseCache (exact)
root @prisma/adapter-d1 dependencies toolchain.prismaAdapterD1 (exact)
root @prisma/client dependencies toolchain.prismaClient (exact)
root hono dependencies toolchain.hono (exact)
root yaml dependencies toolchain.yaml (exact)
root prisma-generator-express dependencies toolchain.prismaGeneratorExpress (exact)
root prisma-guard dependencies toolchain.prismaGuard (exact)
root jsonc-parser dependencies toolchain.jsoncParser (exact)
root @cloudflare/workers-types devDependencies toolchain.workersTypes (exact)
root eav-to-prisma devDependencies toolchain.eavToPrisma (exact)
root @prisma/adapter-better-sqlite3 devDependencies toolchain.prismaAdapterBetterSqlite3 (exact)
root @types/node devDependencies toolchain.nodeTypes (exact)
root better-sqlite3 devDependencies toolchain.betterSqlite3 (exact)
root esbuild devDependencies toolchain.esbuild (exact)
root prisma devDependencies toolchain.prisma (exact)
root typescript devDependencies toolchain.typescript (exact)
root wrangler devDependencies toolchain.wrangler (exact)
apps/site @astrojs/cloudflare dependencies ^12.6.3
apps/site @tailwindcss/vite dependencies ^4.1.14
apps/site astro dependencies ^5.14.1
apps/site tailwindcss dependencies ^4.1.14
apps/site @astrojs/check devDependencies ^0.9.5
apps/site @emnapi/runtime devDependencies ^1.11.3
apps/site typescript devDependencies ^5.9.3

Runtime root packages provide the D1 adapter/client, Hono, guard support, generated-route helpers, and YAML OpenAPI serialization. Root development packages provide Worker/Node types, model conversion, local SQLite serving, Prisma generation/migration, TypeScript checking, esbuild bundling, and Wrangler. Local SQLite packages are development dependencies even though the local server uses them at runtime. The site dependency block contains Astro, its Cloudflare adapter, Tailwind, and the Vite integration. Its development block contains Astro checking and TypeScript.

The root declares Node >=22. Emitted .nvmrc selects 24.18.1 and packageManager selects [email protected]. Existing .nvmrc and package-manager choices are preserved, so the verified installer still checks that the selected npm matches the declaration. Prisma CLI and client must use the supported major 6. Do not infer compatibility from Node’s broad engine range alone.

Terminal window
node scripts/install-dependencies.mjs
node scripts/install-dependencies.mjs apps/site

The verified installer extracts dependencies with scripts disabled, audits lifecycle scripts including native builds, and rebuilds only approved packages. allowScripts approvals name exact versions. Denials can name a package. Root approvals derive from pinned Prisma engines/CLI, better-sqlite3, esbuild, and the recognized Wrangler/workerd pair. Automatic Prisma-client generation and fsevents are denied. The site has its own reviewed allowlist. An unknown lifecycle version requires review. Changing a dependency does not grant script permission. No Git hook is installed.

The Worker reads D1 result types through src/d1-column-types.ts and the site’s src/d1-schema-columns.ts. The D1 adapter guesses a column’s type from its first value, so text that looks like a date can turn a whole column into dates. A raw query ($queryRaw*) gets such a column back as text only when its SQL text proves the column is String:

  • Every row source is a table the schema declares. Sources are read after FROM, every JOIN kind, UPDATE, INTO and each comma of a FROM list, including after ON or USING, at any depth. A table may be quoted, prefixed with main. and aliased. A subquery counts through the tables inside it.
  • At least one source declares the column, and every source that declares it declares String.
  • No select or RETURNING list gives that name to anything except the same column (x, t.x, x AS x).
  • The statement has no UNION, INTERSECT or EXCEPT.

Anything else keeps the adapter’s guess. This happens for a table the schema does not declare, a CTE, a table-valued function and another schema. It also happens for a VALUES list, an alias, an expression, a compound select and a statement with no table. The check reads table and column names from the SQL text. It does not infer the types of SQL expressions.

Item Source or purpose
API Worker The build uses nodejs_compat. Deployment uses the selected repository commit.
D1 database and DB binding Deployment resolves it by site identity and creates it if absent. Migrations and snapshot loading target this database.
SITE_ID, ACCESS_CONFIG, GIT_SHA Worker variables for site identity, read-token digests/scopes, and deployed commit. ACCESS_CONFIG is the union of CMS-issued tokens (access/read-tokens.json) and vars.ACCESS_CONFIG from your wrangler.jsonc, or a valid empty token list when there are none.
API_BEARER_TOKEN Must have at least 32 characters. Store it as the API Worker writer secret in the repository secrets. Never commit it.
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID GitHub Actions secrets used by deployment, not CMS runtime credentials.
PUBLIC_HOSTNAME Repository variable naming the bare hostname used for deployment verification.
Astro site Worker/assets Built from apps/site, bundled with wrangler deploy --dry-run before generation adopts it and before the first provider change, and deployed after API verification. No nodejs_compat: images pass through unchanged (imageService: 'passthrough'), so the Worker does not include sharp. /_image answers 404 (src/lib/image-endpoint.ts). The site has no image service. Sessions use the null driver, so the Worker needs no SESSION KV binding. public/.assetsignore keeps _worker.js and _routes.json out of the public assets.
Adopter media storage R2/storage declarations and transfer manifest are required when moving media. MEDIA_PUBLIC_ORIGIN identifies the destination origin. Media bytes are not source files.
Managed build authority DEE_WAN_URL and a site/version-bound build token belong to managed CI. Repository-only deployment does not fetch CMS definitions.

wrangler.jsonc is a shared local configuration. wrangler.build.jsonc is generated for bundling. Deployment derives wrangler.deploy.jsonc from verified site/database/media inputs. An extra binding in the shared local file is not automatically provisioned or copied into that deployment configuration. Define the required provisioning/deployment step explicitly in an additional workflow. The generator ships no account credentials, active database identifier, or production route in its example configuration.

Published versions become rows. Drafts, unpublished changes, history, users, roles, sessions, credentials and review/audit records stay in Dee Wan. A model slug produces a PascalCase Prisma model and mapped SQL tables/columns. Item identity is preserved in id.

Translatable fields live in a translation table keyed to the base row and language. A to-one relation (oneToOne, manyToOne) keeps target identity in a scalar <field>_id column. A list relation (manyToMany, oneToMany) uses an explicit junction table with ordered positions, owned by the model that declares the field. A one-to-many junction makes its target column unique: each target belongs to one owner through that field. A field that allows several models has one junction per allowed model, each with a foreign key to its model, and they share the owner’s list order through position; a mixed to-one field is stored the same way. The target model gets no column and no write for the field. Single-model output does not invent target models or SQL foreign-key constraints, and it refuses list relations. A hierarchical parent link points at its own model, so it gets a foreign key on both paths. Text/rich text, numbers, booleans and dates become matching scalar columns. JSON, multi-select and components use JSON columns. Select options remain manifest constraints rather than database enums.

In whole-site output every relation id is a foreign key, and deletes follow the CMS:

Reference When the target row is deleted
Optional to-one <field>_id The column becomes null.
Required to-one <field>_id The delete is refused with 409 relation_target_referenced, naming the model and column.
Junction link The link row is removed. The list may then hold fewer items than its minimum until it is written again.
Translation row Removed with its base row.

A relation id inside a component JSON value is not a foreign key. Readers treat an id that resolves to no row as absent, as the CMS does.

Media keeps an identity column and adds a URL column. Moving the repository does not move media bytes. Media transfer rewrites addresses to adopter storage, and deployment verification refuses remaining Dee Wan-owned URLs. Page models produce Astro routes from their title and page document. Ordinary data models receive API routes without invented site pages.

managed-renderer/ exists only when whole-site generation ran for a site with an active managed renderer release. It is export format 2, in the generated zone: generation writes it, fingerprints it and pins it in graduation.manifest.json under managedRenderer. Managed regeneration never rewrites it. The pin’s exportDigest is the SHA-256 of release/release.json, which is canonical JSON followed by one newline. Generation inspects the export from its bytes before it adopts the tree. It refuses an identity that does not match the recomputed value, a release/managed-renderer.mjs that is not byte for byte the command this product emits (managed_renderer_export_cli_mismatch), an unreadable pin (managed_renderer_export_pin_invalid) and a managed-renderer directory spelled in another case. There is no compatibility path for older export shapes. See managed templates.

Generation adds three root scripts. It adds each only when it is absent and never overwrites one.

Script What it does
managed-renderer:verify Checks the manifest pin and the closed release record; every file under managed-renderer/ except build output at its root, by length and digest, below the per-file limit, at a safe path, once even ignoring case; the compiled chunks; the input, source, contract and release digests; the gate report, bound to the release with every required gate passed in order; template.json, route-map.json, site-chrome.json and wrangler.jsonc; and no custody key or local path in the metadata. It does not re-parse the template manifest, its provenance or the route map grammar. Generation’s inspector does.
managed-renderer:build Verifies, then runs npm ci --ignore-scripts, the type check and the build inside managed-renderer/, on this machine and with network for npm ci. It refuses a changed lockfile or another toolchain. It does not re-run the contract, accessibility or viewport gates. Their evidence is release/evidence/report.json.
managed-renderer:prepare Builds, then runs wrangler deploy --dry-run into managed-renderer/.wrangler/deploy. Deploys nothing.

Run them from the repository root. managed-renderer/ and managed-renderer/release/ must be real directories. build and prepare first remove node_modules, dist, .astro and .wrangler at the top of managed-renderer/, and unlink a link there without following it. The commands set ASTRO_TELEMETRY_DISABLED=1 and WRANGLER_SEND_METRICS=false. None of them contacts the CMS.

Script edits keep every other byte of package.json: line endings, indentation, a byte order mark and the final newline. When the previous manifest pinned an export and a later generation no longer writes one, it removes only scripts whose value is still exactly the canonical one, so your own command under one of those names stays. Adding refuses with managed_renderer_scripts_unwritable, and removing with managed_renderer_scripts_unremovable, when package.json is not UTF-8 or strict JSON, its root or scripts is not an object, or it names scripts or one of these scripts twice. Adding also refuses when package.json is missing. A removal from a package.json that never names one of these scripts changes nothing.

The generated workflows, deploy-from-repository.yml and the dee-wan-build.yml that calls it, deploy the API Worker and apps/site only. They never build or deploy managed-renderer/. Its wrangler.jsonc names no route. Whoever deploys it must serve it at https:// plus the site’s active binding host, because the release is pinned to that origin. The renderer reads published content from /v1 at that origin on every request, so publishing needs no rebuild, and it answers HTML with Cache-Control: no-store. It has no service binding: its wrangler.jsonc sets PUBLIC_API_TRANSPORT to "public" and enables the global_fetch_strictly_public compatibility flag, so each read uses public HTTPS and reaches the public Worker by its /v1/* route.

Standalone backend Workers use the registry package @dee-wan/[email protected]. Every generated model defaults to cached reads. The global switch is in Settings → Source & delivery → Standalone authentication, cache and exposure. Each model’s choice is on its model page → Standalone API, stored against the model id like its access policy. Reading and saving the global switch require site:infra. The model page opens with read access to that model, site:settings or site:infra. Saving it requires site:infra. Individual model choices and the global choice are recorded in generation input and manifests. Changing only cache settings makes the artifact out of date. Regenerate and deploy to apply them. Invalid stored settings refuse generation until reviewed and saved.

An opted-out model, its translations and its relation tables are excluded from cached reads. Queries reaching any excluded model also bypass caching. Writes still fence caches when caching is enabled. CACHE_ENABLED=false disables both cached reads and write fences. Any value other than true or false refuses every request with cache_switch_invalid. Local SQLite serving is uncached.

The Worker exports CacheCoordinator. Example, build and deployment Wrangler configurations declare the CACHE_COORDINATOR binding and a SQLite Durable Object migration. Exactly one coordinator name must serve each artifact D1 across every writer and code version. Names must never be shared by different D1 databases. The repository deploy script binds CACHE_COORDINATOR_NAME to the artifact identity plus its resolved D1 identity, preserving compatible owned bindings, migrations and variables. When deploying manually, keep that name stable and explicitly change it when cloning to a different D1. A second coordinator serving the same D1 under a different name would miss writes made through the first coordinator.

On Prisma 6.19.3 the client applies guards before caching. Guards run even on cache hits, and rewritten query arguments enter the cache key. A client is scoped after authorization to the principal, access variant, request URL, method and result-affecting headers, including locale and tenant context. Volatile request identifiers are excluded. Scope values are digests, not stored raw credentials. Auth lookups use uncached reads. Cache integration does not grant any model or operation access and has no Dee Wan runtime callback.

D1 data:load requires the artifact service writer token and PUBLIC_HOSTNAME before it writes. After a verified committed load, it calls the service-only invalidation endpoint, which verifies the committed snapshot ledger and calls $cacheInvalidate({ all: true }). Failure is a failed command, even if data already committed. Replaying the same load retries invalidation. A local D1 load also needs a loopback CACHE_INVALIDATION_ORIGIN. Local SQLite file loads need no coordinator. Raw writes made by other tools must use $cacheRawWrite or explicit post-commit invalidation. Post-commit invalidation has a window where older cached data may be read, so keep traffic stopped during bulk loads.

Generation is tied to the artifact manifest and an optional operator CACHE_GENERATION epoch. Different enabled generations sharing a coordinator remain correct but repeatedly invalidate each other’s caches and serve requests without cached data. Enabled and disabled versions must never serve together: disabled writes are unfenced. Stop and drain traffic, deploy all writers together, and invalidate or change CACHE_GENERATION before re-enabling after uncached writes. Do not use a gradual rollout to change the global mode.

Defaults are a one-hour fresh TTL and a 1 GiB storage budget. Stale-while-revalidate uses request ctx.waitUntil. Without request context it does not refresh in the background. A cache hit costs one Durable Object call. A miss adds a database read and a fill call. Writes normally use two coordinator calls. One coordinator has a soft limit of about 1,000 requests per second and one location, so network distance may make hits slower than D1 reads. Read-cache failures fall back to D1. Failure to acquire a write lease refuses the write. Loss of the final write fence can leave an entry until its TTL after the lease expires. Review the package contract when handling an outage.

Durable Object storage migrations are forward-only. Older code refuses newer storage, including Prisma writes. Roll forward across storage format changes. A code rollback is not a database, cache-storage or media rollback. Keep compatible migration history and adopter backups.

Return to Standalone backend: Overview, or continue with the product vocabulary and the rest of the reference pages.


Unreleased / pre-MVP; no stable version
Source revision: fe23db481c422b40e317b92fc98116b4c0c26a87 (modified checkout)
Build date (UTC): 2026-10-18T09:22:34.630Z
Upgrade target: unavailable: modified checkout. Release notes