Extending your backend
You do not need to detach to customize the generated repository. Owned and shared extension surfaces work while the repository stays connected to Dee Wan. Detach only when you want Dee Wan-managed regeneration to stop. Standalone backend: Overview describes the generated backend, and Implementation details lists its dependencies.
Connected workflow
Section titled “Connected workflow”- Generate the project locally with
npm run backend:generateornpm run backend:deliver. Settings → Source & delivery cannot compile source (source_build_runtime_unavailable). - Clone your generated repository.
- Edit owned/shared files, starting with
src/owned/backend.tsfor API customization. - Run the artifact’s checks, commit and push your changes.
- Deploy that exact repository commit through
deploy-from-repository.yml. - Later, regenerate from Dee Wan. Owned/shared work is preserved. The generator refuses to run if generated files have been edited.
- Detach only if you want managed regeneration to stop.
The GitHub authentication matrix explains the authority needed for delivery. The detachment runbook covers independence checks. Customization is not a reason to detach.
What can I change while connected?
Section titled “What can I change while connected?”| Change | Where | Regeneration behavior |
|---|---|---|
| Generated-operation enable/disable, hooks, same-contract override | src/owned/backend.ts |
Preserved. Validation or construction fails if operation IDs have been removed, settings conflict or group expansions have changed. Generated security remains outside owned policy. |
| Declarative custom API routes | Same registry, using route() |
Preserved. Collisions with generated or custom routes are rejected after paths are normalized. Valid schemas enter OpenAPI and validate input/output. |
| Raw Hono escape hatch | src/owned/routes.ts |
Preserved and mounted last. Cannot replace generated paths, including disabled operations. No collision guarantee or OpenAPI for raw routes. |
| Reusable business logic | Other files below src/owned/ |
Preserved byte-for-byte. Import it from the registry or raw routes. |
| Own database tables and migrations | Owned SQL or a runner below src/owned/, with your own workflow |
Source and unrelated tables/rows survive. Run your migration explicitly. It is not automatically added to generated Prisma schema or the generated migration runner. Do not modify generated tables. |
| Dependencies and version ownership | Root and apps/site/package.json |
Additive merge keeps your existing values. Missing required dependency/override keys are added. Your chosen version must still satisfy build/runtime contracts. |
| Wrangler bindings and secrets | Shared root/site wrangler.jsonc, own deployment workflow and secret store |
Shared source is preserved. Extra local bindings are not automatically provisioned or copied into generated wrangler.deploy.jsonc. Generated build/deploy configs remain generated. Never commit secrets. |
| Own CI workflows | .github/workflows/<your-name>.yml |
Preserved, except the two generated workflow names. |
| Astro head/body and layout slots | apps/site/src/owned/Head.astro, BodyEnd.astro, slots/ |
Preserved. Generated layouts import them. |
| Section wrappers and overrides | apps/site/src/owned/sections/ |
Preserved. Generated section contracts still typecheck against your implementation. |
| Integrations and islands | apps/site/src/owned/config.mjs, owned components, site dependencies |
Preserved. Add the integration and its dependencies together, then run Astro checking/building. |
| Own pages | apps/site/src/pages/owned/ |
Preserved and served under /owned/. Generated pages and redirects cannot claim that namespace. |
| Generated files and content rows | Schema, generated clients/routers/security, snapshots, page data, generated content tables | Do not edit while connected. Source fingerprints, snapshot digests and row-set checks refuse drift. Change CMS models/content or use an owned surface. |
README.md, .nvmrc, .npmrc, .gitignore and existing shared Wrangler files are retained. deploy-from-repository.yml is generated, and dee-wan-build.yml with it in a whole-site artifact (a --model artifact has no CMS-managed site build). Your other workflows are shared. Existing SQL migration history is preserved. Do not rewrite an applied migration.
Zone of sample paths
Section titled “Zone of sample paths”| Path | Zone |
|---|---|
prisma/schema.prisma |
generated |
graduation.manifest.json |
generated |
src/app.ts |
generated |
src/worker.ts |
generated |
src/security.ts |
generated |
src/owned/backend.ts |
owned |
src/owned/routes.ts |
owned |
src/backend-customization.ts |
generated |
.dee-wan-customization.json |
generated |
src/serve-cli.ts |
generated |
migrations/0002_graduation_ledger.sql |
generated |
scripts/regenerate.mjs |
generated |
scripts/emit-migration.mjs |
generated |
scripts/file-manifest.mjs |
generated |
scripts/portable-client.mjs |
generated |
scripts/configure-deployment.mjs |
generated |
tsconfig.json |
generated |
wrangler.example.jsonc |
generated |
wrangler.build.jsonc |
generated |
scripts/load-snapshot.mjs |
generated |
data/published-snapshot.json |
generated |
.github/workflows/dee-wan-build.yml |
generated |
.github/workflows/deploy-from-repository.yml |
generated |
.github/workflows/anything-else.yml |
shared |
package.json |
shared |
package-lock.json |
shared |
.npmrc |
shared |
.nvmrc |
shared |
wrangler.jsonc |
shared |
.gitignore |
shared |
README.md |
shared |
apps/site/src/owned/config.mjs |
owned |
apps/site/src/pages/owned/thanks.astro |
owned |
apps/site/package.json |
shared |
apps/site/package-lock.json |
shared |
apps/site/.npmrc |
shared |
apps/site/wrangler.jsonc |
shared |
apps/site/src/lib/pages.json |
generated |
Typed operation registry
Section titled “Typed operation registry”src/owned/backend.ts is scaffolded only when absent. Regeneration never rewrites an existing registry. npm run customization:check restores an absent registry as an empty customization and validates the generated catalogue. The generated app imports it. Route registration order does not determine which implementation runs.
Examples below assume the emitted model is Article. Use the actual IDs and Prisma fields from src/backend-customization.ts. Each example is a complete alternative registry. Combine its entries in one defineBackend() call when using several features.
One request hook
Section titled “One request hook”import { defineBackend } from '../backend-customization'
export default defineBackend({ operations: { 'Article.findMany': { before: [(c) => { c.header('x-content-policy', 'owned') }], }, },})before and after are request-execution hooks. They are not Prisma lifecycle hooks, transactions, finally blocks, or after-commit callbacks. A terminal response or thrown error stops the remaining phase. An after hook is not guaranteed to run after failure and is not evidence that a transaction committed.
Wrap the generated implementation
Section titled “Wrap the generated implementation”import { defineBackend } from '../backend-customization'
export default defineBackend({ operations: { 'Article.findMany': { override: async ({ input, args, context, prisma, core }) => { void input void args void context void prisma return [...await core()].reverse() }, }, },})input and args name the same parsed Prisma arguments. context is the verified principal. prisma exposes only the target operation’s guarded delegate. It does not expose raw SQL, a raw client, or other models/operations. Paginated reads expose guarded findMany and count. core() runs the generated implementation with the same request/guard state. It is not a transaction or retry boundary. Call it once unless repeated execution is intended.
Replace one operation
Section titled “Replace one operation”import { defineBackend } from '../backend-customization'
export default defineBackend({ operations: { 'Article.findMany': { override: async () => [], }, },})This replacement returns an empty array under the generated list contract and never calls core(). It still requires authentication, permission and resolved guards. Returning a different envelope, status contract or path requires a new declarative route. Types derive from emitted Prisma arguments/results. Changing or removing a model/operation exposes stale registry references during typechecking or construction.
Disable an operation
Section titled “Disable an operation”import { defineBackend } from '../backend-customization'
export default defineBackend({ operations: { 'Article.delete': { enabled: false } },})enabled: true retains an emitted operation. It cannot enable a route for which the platform emitted no security. Disabled paths remain reserved. An array of declarations for one ID may compose hooks. More than one override, contradictory enable settings, or combining disable with an override is rejected.
Read and write groups
Section titled “Read and write groups”import { defineBackend } from '../backend-customization'
export default defineBackend({ groups: [ { name: 'content-reads', select: 'reads', accepted: ['Article.findMany', 'Article.findManyPaginated', 'Article.findUnique'], before: [(c) => { c.header('x-content-read', 'owned') }], }, { name: 'content-writes', select: 'writes', accepted: ['Article.create', 'Article.update', 'Article.delete'], after: [(c) => { c.header('x-content-write', 'owned') }], }, ],})This example assumes Article is the only emitted model and has no translations/junctions. select: ['Article.findMany', 'Article.findUnique'] selects explicit IDs. Read/write selectors expand across the entire emitted catalogue, including read-only models. Group names are unique, and hook order follows group declaration order.
accepted pins the exact set. customization:check records expanded IDs in .dee-wan-customization.json. Adding or removing a model or operation causes validation to fail if the expansion changes. Review the exact proposed list in the error, update that group’s accepted array in the owned registry, and regenerate/check again. Do not edit the generated state file or derive acceptance with a filter over the current catalogue: that would silently accept future changes.
A group may also carry typed overrides: { 'Article.findMany': async ({ core }) => core() } for an ID in its accepted expansion. A second override from another group or the operation settings is rejected. Declaration order never chooses a winner.
Existing variants and ordering
Section titled “Existing variants and ordering”Variant hooks use operations['Article.findMany'].variants.reader.before/after or the generated editor variant. They compose with the generator’s existing variant mechanism. Unknown variants and attempts to supply shapes or router-level security settings from owned code are rejected. There is no second variant dispatcher.
Execution order is: platform authentication/permission, guard resolution, group before, operation before, variant before, then the generated or override handler. After the handler: variant after, operation after, group after, outer response/error protection. A terminal response/error preserves upstream short-circuit behavior. Hooks must preserve the documented request/result shape.
Add a declarative custom route
Section titled “Add a declarative custom route”import { defineBackend, route } from '../backend-customization'
export default defineBackend({ routes: [route({ method: 'POST', path: '/echo', access: { mode: 'service', operation: 'create' }, request: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false, }, response: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false, }, handler: async ({ input }) => ({ text: input.text }), })],})The path is relative to /api. Supported methods are GET, POST, PUT, PATCH and DELETE. Declare access.operation as read/create/update/delete. Optional access.model names an emitted model. Without a model scope, a read token needs wildcard * scope. The handler receives validated typed input, the verified principal and path parameters. No raw client is provided.
Declare each custom route with route(). Construction rejects copied or manually assembled route descriptors. Request/response schemas are captured at declaration, so later mutation cannot change validation or OpenAPI.
Schemas support string/enum, finite number, safe integer, boolean, null, arrays, and closed objects with declared required fields. Unsupported schema keywords are rejected instead of appearing as enforced OpenAPI rules. GET query properties must be strings. Other methods parse a JSON body. Invalid input returns 400. A result violating the response schema returns 500.
Duplicate methods and normalized paths are rejected across generated/declarative routes, even if a generated operation is disabled. Repeated/trailing slashes normalize. Wildcard, encoded, query/fragment and traversal path syntax is rejected. Parameter-name aliases and static/parameter overlap are rejected as well. A generated collision must be an operation override. Valid declarative schemas appear in authenticated /api/openapi.json. Raw Hono routes do not.
Raw Hono escape hatch
Section titled “Raw Hono escape hatch”Keep imperative integrations in src/owned/routes.ts. It remains mounted after generated and declarative routes, with reserved generated paths ahead of it. Raw routes receive the outer authentication/error protection, but have no declarative collision guarantee, automatic request/result schema validation, or generated OpenAPI entry.
The compatibility mount still infers model scope by path prefix. A raw path matching no model needs wildcard read scope. Apply explicit .guard(shape, variant) to raw-route Prisma queries. An extended client alone does not guard arbitrary calls. Prefer declarative access metadata when the path does not represent a generated model.
Access is checked before your code
Section titled “Access is checked before your code”Access policy decides whether a route accepts anonymous, session or service callers. Missing model policy is service. Read tokens are digests scoped by model in ACCESS_CONFIG. Unscoped routes and /api/openapi.json require wildcard scope. Service writes without a writer authenticator return 503 writes_unavailable. Absent credentials return 401 and denied permission returns 403. Optional Better Auth supplies the artifact session for both local and Worker runtimes. Local serving and the Worker accept the same API_BEARER_TOKEN service bearer.
Errors remain normalized/redacted outside overrides and hooks. New response shapes use new routes. Extra authorization belongs in your business logic and cannot remove the platform checks.
Add a site extension
Section titled “Add a site extension”Edit apps/site/src/owned/Head.astro:
<meta name="color-scheme" content="light dark" />For a new page, add apps/site/src/pages/owned/thanks.astro. For an island, add its framework integration/dependencies and configure apps/site/src/owned/config.mjs. The generated Astro configuration imports that owned configuration. Head.astro, BodyEnd.astro, layout slots and section wrappers/overrides are independent extension surfaces. The preview bridge remains optional in extensions.ts.
Dependencies, migrations and regeneration
Section titled “Dependencies, migrations and regeneration”Both package manifests preserve existing dependency/override values and add missing required entries. The root also adds missing script names. Existing commands remain yours. Generated build/deploy workflows run the customization check directly, even when an older owned typecheck command is retained. Package-manager and lifecycle-script decisions remain yours. An incompatible version fails checking/building. Root pins record the generating toolchain. Site ranges are fixed by the site’s lockfile. The build rejects an unreadable manifest. Install through the verified installer after changes.
Keep business logic, own SQL and migration runners below src/owned/. Run application-owned migrations from an owned workflow. Generated Prisma models describe CMS content only. Adding an application table does not automatically add a generated model or endpoint. Preserve your migration order and rollback plan.
Regeneration compares generated fingerprints, preserves owned/shared files, appends required migrations and refreshes generated content. Regeneration rejects edited generated files and altered snapshots. When a changed snapshot is loaded, it replaces the rows of every generated content table: a row the snapshot does not hold is deleted, whoever wrote it. Your own tables are not part of the snapshot and keep their rows.
npm run regenerate needs the site definitions in definitions.json and GENERATION_SOURCE_VERSION. Managed CI gets the version from the CMS and pulls the definitions. Regenerate locally shows how to get them. With both in place:
node scripts/install-dependencies.mjsGENERATION_SOURCE_VERSION=<version> npm run regeneratenpm run migrate:emitnpm run typechecknode scripts/install-dependencies.mjs apps/sitenpm run --prefix apps/site checknpm run --prefix apps/site buildnpm run buildnpm run files:manifestDeploy
Section titled “Deploy”Commit/push the reviewed repository and run deploy-from-repository.yml for its exact git_sha. Configure CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, API_BEARER_TOKEN and PUBLIC_HOSTNAME as described in Cloudflare resources and configuration. Managed regeneration calls this same deployment workflow after committing generated updates.
For local serving, use db:init, build:serve and serve:local with local access configuration and a writer bearer as shown in the emitted README. npm run dev runs Wrangler locally. Prove unauthenticated refusal, permitted reads/writes, guard refusals, override results and error redaction before deploying. An earlier code commit is not a database backup: rollback must also respect migration and media compatibility.
Optional artifact authentication and access
Section titled “Optional artifact authentication and access”In Settings → Source & delivery → Standalone authentication, cache and exposure, enable Better Auth once for the backend. Select Password, Magic link (Resend), Google or GitHub. Disabled is the default: no Better Auth dependency, runtime, routes, tables, migration, provider configuration or secrets are emitted. CMS accounts, sessions, invitations, password hashes and provider credentials never enter a standalone backend. Application content named User stays separate from the artifact identity tables.
| Access mode | Accepted caller |
|---|---|
public |
Accepts an explicit typed anonymous principal without a credential. |
session |
A current session from this artifact’s Better Auth instance. |
service |
API_BEARER_TOKEN, or an eligible scoped token for a read. |
session_or_service |
Either the artifact session or service authentication. |
A missing model configuration explicitly uses service. Missing operation overrides inherit the model default. Overrides use exact generated IDs such as User.findMany, User.findManyPaginated, User.findUnique, User.create, User.update and User.delete. Validation fails for renamed or removed models or operations. Generated policy types also reject stale IDs. Operation availability remains in the owned backend registry. Access settings do not re-enable disabled operations. Policies name sessions, never particular login providers.
Authentication and permission checks run before hooks and overrides. Guard resolution, existing ownership boundaries and error protection remain mandatory. A public create rejects protected system, owner, tenant, role, administrator, password, verification and provider-link fields, including nested input. Public create makes application content. Signup happens only through Better Auth routes. Raw routes default to service. Declarative routes must state access.mode and access.operation, with optional model scope. OpenAPI shows each operation’s access mode.
| Optional install root | Package | Block | Exact version |
|---|---|---|---|
root, only when enabled |
better-auth |
dependencies |
1.6.25 |
Enabled artifacts emit src/better-auth.ts, provider verification and the existing auth limit/lockout rules, plus a separate migration for _artifact_auth_* infrastructure tables. These tables have no generated CRUD endpoints. Both the Node/SQLite server and Worker/D1 use the same selected methods, password bounds, verified OAuth identities, hashed verification tokens and database session revocation. Both also use the seven-day session limit and mutation-origin checks. Provider access, refresh and ID tokens are discarded. Auth responses suppress provider internals.
Run npm run auth:init to create a fresh random secret in ignored .dev.vars. It preserves an existing artifact secret. The Node server can load this file with node --env-file=.dev.vars src/serve.local.mjs. Wrangler loads it directly. Apply db:init first. Set ARTIFACT_ORIGIN to the API origin, for example http://127.0.0.1:8787 locally. Remote origins must use HTTPS. Deployment derives the origin from the reviewed target hostname, never from the CMS origin.
| Selected method | Required runtime values | Target callback |
|---|---|---|
| Password | BETTER_AUTH_SECRET |
Password routes under /api/auth |
| Magic link | BETTER_AUTH_SECRET, RESEND_API_KEY, MAIL_FROM |
<ARTIFACT_ORIGIN>/api/auth/magic-link/verify |
BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
<ARTIFACT_ORIGIN>/api/auth/callback/google |
|
| GitHub | BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
<ARTIFACT_ORIGIN>/api/auth/callback/github |
Register callbacks in adopter-owned OAuth applications. Resend needs a verified sender domain. Only selected methods require credentials. Target preflight fails before deployment when a selected method lacks them. Repository Actions secrets prefix runtime names with ARTIFACT_. This also avoids GitHub’s reserved GITHUB_ prefix. Delivery generates a new artifact secret and seals it to the target public key. Existing target secret names are preserved without rotation. npm run backend:deliver reads provider inputs only from ARTIFACT_* environment variables. Secret values never enter source, manifests, generation inputs, digests, checked-in vars, logs or deployment ledgers.
Regeneration preserves owned/shared files. Enabling auth in an existing shared package or changing selected methods requires regeneration to update dependencies and caller workflow bindings. Disabling auth on an existing destination is rejected: use a new destination, preserving the old identities and migration history. Access-only changes participate in generation-source and manifest digests.
verify:deployment respects access policy: public reads are checked anonymously. Service reads use the bearer. Session-only reads require a current target session in ARTIFACT_VERIFY_SESSION_COOKIE. Without it, verification refuses with verify_session_required. It never reports an unperformed content check as passed. For first deployment, initialize the target, create an account through its auth routes, and rerun verification with that session. Keep the short-lived session out of source and evidence. Browser and live deployment acceptance remain separate gates.
Next step
Section titled “Next step”Define the queries your routes accept in query variants and guards.
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