ข้ามไปยังเนื้อหา
Pre-MVP โค้ดเบส 1.0 ยังไม่ใช่ผลิตภัณฑ์ที่เผยแพร่ — ดู ขอบเขตผลิตภัณฑ์ 1.0

Dee Wan CMS

Dee Wan CMS — ตราตัวอักษร Dee Wan บนแบนเนอร์ลายไทย

Dee Wan CMS (ดีหวาน 🇹🇭) คือ headless CMS แบบ multi-tenant และหลายภาษา ที่ทำงาน บน edge ของ Cloudflare ทั้งหมด: Hono Worker บน D1 สำหรับ API และส่วนผู้ดูแลที่เป็น SvelteKit บน Pages

มันเป็น CMS เป็นอันดับแรก: สร้างโมเดลเนื้อหา เขียนเนื้อหา เผยแพร่ และจัดการเนื้อหา สิ่งที่ทำให้มันแตกต่างจาก headless CMS อื่นๆ คือสิ่งที่มันส่งมอบให้คุณได้ในตอนท้าย — Generate standalone backend (สร้างแบ็กเอนด์แบบ standalone) เปลี่ยนไซต์ให้เป็น repository ที่ผู้รับไปดูแล (adopter) เป็นเจ้าของ พร้อม Prisma schema ที่ normalize แล้ว, migration แบบต่อท้ายอย่างเดียว (append-only) ที่ตรวจทานได้, route ที่สร้างขึ้นซึ่งใช้ semantics ของ model/query แบบ Prisma ที่คุ้นเคย, ฐานข้อมูล การ deploy และ credential ของตัวเอง และไม่มีการพึ่งพา Dee Wan ขณะ runtime โค้ดที่นักพัฒนาเป็นเจ้าของยังคงอยู่หลังการสร้างใหม่ (regeneration) และ การแยกตัว (detach) ออกจาก Dee Wan โดยสิ้นเชิงเป็นผลลัพธ์ที่รองรับ ไม่ใช่ทางหนีฉุกเฉิน

จุดแข็งเสริม: การ deploy แบบ Cloudflare-native, โมเดลการพัฒนาแบบ Prisma ที่คุ้นเคย, การทำงานแบบหลายไซต์ และหลายภาษา, repository และโครงสร้างพื้นฐานที่เป็นอิสระ และการพัฒนา schema อย่างปลอดภัย การ import/export, editorial workflow ที่เลือกใช้ได้, การช่วยเหลือด้วย AI และเครื่องมือย้ายพอร์ตโฟลิโอเป็น ความสามารถเสริม — ไม่มีสิ่งใดในนี้เป็นนิยามของผลิตภัณฑ์

มีพื้นผิวการส่งมอบสองแบบ และทั้งสองไม่ใช่สิ่งเดียวกัน CMS public API คือพื้นผิวการเผยแพร่ แบบ live และอัปเดตทันทีที่เนื้อหาถูกเผยแพร่ แบ็กเอนด์ที่สร้างขึ้น คือ รีลีสของ schema และเนื้อหาที่เป็นอิสระ: การเปลี่ยนแปลงใน CMS ภายหลังจะไปถึงมันได้ผ่าน regeneration และการ deploy ที่สั่งอย่างชัดแจ้งเท่านั้น มันไม่ได้ถูกซิงก์อย่างต่อเนื่อง

Pre-MVP: codebase 1.0 ยังไม่ใช่ผลิตภัณฑ์ที่รีลีสแล้ว การทดสอบแบบ local ครอบคลุมมาก แต่ เกณฑ์การยอมรับบน Cloudflare จริง, การตรวจ browser/visual ขั้นสุดท้าย และการย้ายสี่ไซต์ยังคงค้างอยู่ ดู เกณฑ์รีลีส (SPEC.md, in the repository) และ ขอบเขตผลิตภัณฑ์ 1.0

ประเภทเนื้อหาไม่ได้ถูก hardcode ไว้ คุณกำหนด โมเดลเนื้อหา และฟิลด์ของมัน ใน UI ตัวสร้างโมเดล และ CMS จะเก็บเป็นข้อมูลแบบ EAV — ดังนั้นการเพิ่มประเภท “Recipe” พร้อมฟิลด์ที่คุณต้องการจึงไม่ต้องเขียนโค้ดและไม่ต้องมี migration

การตั้งค่าฐานข้อมูลเริ่มจาก Prisma migration เริ่มต้นหนึ่งชุดและ mirror สำหรับ D1 ที่สร้างจากมัน bootstrap ของ tenant มี schema รายไซต์ปัจจุบันอยู่ รีลีสในอนาคตจะต่อท้าย migration ดู คำแนะนำเรื่อง migration ของฐานข้อมูล

  • ตัวสร้างโมเดล — กำหนดโมเดลเนื้อหาและฟิลด์ (text, rich text, number, boolean, select, date, media, JSON, relation, component) ผ่าน UI การ apply การเปลี่ยนแปลงจะทำ diff ก่อน และการแก้ไขที่จะทำให้เนื้อหาที่มีอยู่ไม่ถูกต้องจะถูก แจ้งเตือนก่อนที่จะรัน

  • หลายไซต์ — การ deploy หนึ่งชุดให้บริการได้หลายไซต์ ทุก request ฝั่งผู้ดูแลแนบ X-Site-Id และชั้นข้อมูลบังคับใส่ site_id ลงในทุก query; API ปฏิเสธที่จะเดา tenant แทนที่จะเสี่ยงให้เกิดการอ่านข้าม tenant

  • หลายภาษา — ตั้งค่าภาษาใน UI; เนื้อหาถูกเก็บแยกตามภาษา

  • การจัดการเวอร์ชันและการเผยแพร่ — revision ของเนื้อหา พร้อมการเผยแพร่/ยกเลิกการเผยแพร่ การตรวจทานเชิงบรรณาธิการเป็น โหมดรายไซต์ที่เลือกใช้ได้ และ ปิดอยู่โดยค่าเริ่มต้น: ไซต์ใหม่เผยแพร่ตรงจาก draft และจะไม่มี ตัวควบคุมการตรวจทานแสดงขึ้นจนกว่าจะมีคนเปิดใช้ใน Settings → General

  • การช่วยเหลือด้วย AI — สร้างและแปลข้อความ และสร้างภาพ ผ่าน proxy ฝั่งเซิร์ฟเวอร์ key ของผู้ให้บริการไม่เคยไปถึง browser

  • Media — อัปโหลดไปยัง R2 หรือ Cloudflare Images พร้อมแปลงเป็น webp/avif อัตโนมัติ

  • Auth — session ที่โฮสต์เอง: ลิงก์ claim แบบใช้ครั้งเดียวสร้าง ผู้ดูแลคนแรก รหัสผ่านถูกตั้งโดยเจ้าของ และทุก request มี session ในฐานข้อมูล Cloudflare Access เป็นการเสริมความแข็งแรงของขอบเขต (perimeter) ที่เลือกใช้ได้ ปิดอยู่โดย ค่าเริ่มต้น ดู SPEC-AUTH.md

  • ปลั๊กอินแบบ remoteปลั๊กอินคือ remote service ที่ deploy แยกต่างหาก มันไม่รันโค้ดใดๆ ภายใน Dee Wan: ไม่มี module ถูกโหลด ไม่มี JavaScript ไปถึง admin UI ไม่มี database binding ถูกส่ง ออกไป มันสื่อสารผ่าน HTTP protocol ที่มีเวอร์ชันเพียงหนึ่งเดียว ภายใต้ grant (สิทธิ์ที่มอบให้) รายไซต์ที่เลือกด้วยมือใน Settings → Plugins และสามารถอ่านสรุปเนื้อหาแบบจำกัด และขอการเผยแพร่/ยกเลิกการเผยแพร่แบบมีเงื่อนไข หนึ่งครั้ง การเผยแพร่ตามกำหนดเวลาคือปลั๊กอิน Scheduling ไม่ใช่ฟีเจอร์หลัก

    เอกสารสำหรับนักพัฒนา: การเขียนปลั๊กอิน · protocol v1 · ความปลอดภัย · การทดสอบ schema ที่เครื่องอ่านได้ของ manifest และ protocol ถูกสร้างจาก นิยามเดียวกับที่ route ใช้ validate ข้อกำหนดของแพลตฟอร์มคือ SPEC-PLUGINS.md; ปลั๊กอินอ้างอิงคือ plugins/scheduling/

บทบาท (role) author ที่มีมาในตัวถูกจำกัดตามเจ้าของ รวมถึงการดำเนินการ workflow, restore และ publish การแก้ไข translation group ใช้เจ้าของของ instance หลัก พฤติกรรมทั้งหมดและบันทึกเรื่อง migration อยู่ใน SPEC-ACCESS.md §4.4

ส่วนประกอบ Stack
Admin UI SvelteKit (SPA, ssr=false), shadcn-svelte, TipTap — บน Cloudflare Pages
API Hono บน Cloudflare Worker
ฐานข้อมูล D1 เข้าถึงผ่าน Prisma (@prisma/adapter-d1)
Media R2 หรือ Cloudflare Images
Auth session ของ Better Auth; Cloudflare Access เป็น perimeter ที่เลือกใช้ได้
Inference DeepInfra

repo นี้เป็น npm workspace ไลบรารีที่ใช้ร่วมกันอยู่ใน packages/ ในรูปแบบ git submodule — clone ด้วย --recurse-submodules

backend/ Hono Worker: API, auth, scoped CRUD, AI proxy, Prisma schema
frontend/ SvelteKit admin
packages/ schema-parser, prisma-guard, prisma-rbac, eav-to-prisma (submodules)
plugins/ remote plugins built against protocol v1 (Scheduling); not workspaces
scripts/ setup, deploy, migrate, health, teardown

ทุกอย่างหลังส่วนนี้จะสร้างทรัพยากร Cloudflare จริง ส่วนนี้ไม่สร้าง: มันรัน ผลิตภัณฑ์ทั้งหมดบนไฟล์ D1 แบบ local บนเครื่องของคุณ โดยไม่ต้องมีบัญชี ไม่ต้องมี API token และไม่ต้องมี DNS

Terminal window
node scripts/install-dependencies.mjs # locked tree, scripts off, audited, then rebuilt
npm run local:migrate # apply the schema to the local D1 mirror
npm run local:api # admin Worker on http://localhost:8787 (leave running)
npm run local:seed # the fixture rig, then a corpus that looks like real sites
npm run local:admin # admin UI on http://localhost:5173 (leave running)

local:seed พิมพ์ข้อมูลเข้าสู่ระบบที่มันเพิ่งสร้าง — admin@localhost.test รหัสผ่าน local-dev-password-1234 คำสั่งนี้เป็น idempotent: รันซ้ำแล้วมันจะรายงานสิ่งที่มีอยู่แล้ว แทนที่จะ สร้างสำเนาที่สอง

มัน seed สองอย่าง และทั้งสองเป็นคำสั่งแยกกันหากคุณต้องการเพียงอย่างเดียว:

คำสั่ง สิ่งที่สร้าง
npm run local:seed:fixture Local Dev Site: ผู้ดูแลคนแรก, โมเดล Article, draft สามรายการ และ Page ตัวอย่างหนึ่งหน้า นี่คือสิ่งที่ e2e spec ของ real-backend ใช้ตรวจสอบ ดังนั้นเนื้อหาของมันจะไม่เปลี่ยน
npm run local:seed:corpus สองไซต์ที่อ่านแล้วเหมือนของจริง — Meridian Review นิตยสารที่มีคลังย้อนหลังสิบแปดเดือน และ Fern & Ash ไซต์ของสตูดิโอขนาดเล็ก

corpus นี้ deploy preset Blog System ที่มาพร้อมผลิตภัณฑ์ (Post, Article, Category, Keyword และ component หกตัว) ดังนั้นโมเดลจึงเป็นแบบเดียวกับที่ผู้ใช้ได้จากตัวสร้างโมเดล ไม่ใช่ schema ที่คิดขึ้นมาเพื่อการ seed มันเขียนรายการประมาณห้าสิบรายการในสถานะ published, draft, in-review, approved และ scheduled เชื่อมโยง category, keyword และบทความที่เกี่ยวข้อง อัปโหลดภาพพร้อม alt text ใน ทั้งสอง locale และแปลบทความสองบทความเป็นภาษาเยอรมัน ทั้งสองไซต์เป็นเรื่องแต่งและระบุไว้เช่นนั้น; ไม่มี องค์กร บุคคล หรืออาคารจริงปรากฏในไซต์เหล่านั้น

วันที่ถูกยึดกับ timestamp การสร้างที่เก่าที่สุดของสองไซต์ใน corpus ภายในเป้าหมาย D1 local เดียวกัน corpus ใหม่จะวางรายการที่ใหม่ที่สุดไว้ห้าวันก่อนวันนั้น และคงช่วงเวลาที่กำหนดไว้ ข้ามทั้งสองไซต์ การรันซ้ำภายหลังและการซ่อมรายการที่ขาดหายจะใช้จุดยึดเดิม; จะไม่ เลื่อนเนื้อหาที่มีอยู่ไปข้างหน้า การกำหนดค่า timestamp เริ่มต้นจะทำต่อหลังรอบที่ถูกขัดจังหวะ และจะไม่แตะ timestamp ของ update/version ที่เกิดภายหลังเมื่อ instance ถูกกำหนดค่าเริ่มต้นแล้ว ค่าของ article และ post ที่เขียนไว้แล้วจะถูกเก็บไว้; relation ที่ขาดหายอาจยังถูกซ่อมได้ จุดยึดของไซต์ ที่อ่านไม่ได้หรือไม่ตรงกันจะหยุดการรัน สิ่งนี้ไม่ได้อ้างว่าแยกจากการแก้ไขด้วยมือที่เกิดขึ้นพร้อมกัน

ภาพมาจาก backend/scripts/fixtures/media/ เมื่อไดเรกทอรีนั้นมีไฟล์อยู่ — ใส่ภาพถ่าย ลงไปแล้ว corpus จะใช้มัน หากไม่มีอะไรอยู่ มันจะสร้าง placeholder แบบ duotone ที่กำหนดผลได้แน่นอน ซึ่ง เป็น placeholder อย่างตรงไปตรงมา ไม่ใช่ภาพถ่าย

จากนั้นใน admin UI: เปิด Content แก้ไขบทความที่ seed ไว้หนึ่งบทความ แล้วเผยแพร่

แดชบอร์ดผู้ดูแลบนไซต์ local ที่ seed แล้ว: จำนวนเนื้อหาทั้งหมด สิ่งที่ live อยู่ และการเปลี่ยนแปลงล่าสุด

Content: draft ของ Article สามรายการที่ seed ไว้ในรายการ พร้อมตัวกรอง stage และ delivery

บทความที่ seed ไว้เปิดอยู่ใน content editor โดยมีแผง Publishing อยู่ข้างๆ

การอ่านสิ่งที่คุณเผยแพร่ในแบบที่เว็บไซต์อ่าน:

Terminal window
npm run local:public # public read Worker on http://127.0.0.1:8788 (leave running)
curl -H "Host: localhost" http://127.0.0.1:8788/v1/article

header Host ไม่ได้มีไว้ประดับ public Worker ระบุไซต์จาก Host และจาก สิ่งนี้เท่านั้น ดังนั้น request ที่ไม่มีมันจะได้คำตอบ 404 — นั่นคือกฎเดียวกับที่การ deploy ใช้

public cache ปิดอยู่จนกว่าคุณจะเปิด การ deploy จะ purge cache tag ที่ edge เมื่อ เผยแพร่ ซึ่งต้องใช้ CF_PURGE_ZONE_ID และ CF_PURGE_API_TOKEN ที่มีสิทธิ์ Zone → Cache Purge installation (การติดตั้ง Dee Wan หนึ่งชุด) ค่าเริ่มต้นไม่มีทั้งสองอย่าง จึงมาพร้อม PUBLIC_CACHE_MAX_AGE=0 และ public worker ตอบ Cache-Control: no-store: ไม่มีสิ่งใดถูกเก็บและไม่มีสิ่งใดถูกอ่าน ดังนั้นการเผยแพร่ การอัปเดต การยกเลิกการเผยแพร่ หรือการลบจะเห็นได้ในการอ่านครั้งถัดไป ค่าศูนย์ปิด cache ทั้งสองทิศทางโดย เจตนา — entry หนึ่งมีอายุยืนกว่าการตั้งค่าที่เก็บมัน ดังนั้น worker ที่หยุดเขียนแต่ยังคง อ่านอยู่จะให้บริการคำตอบเก่าจากช่วงอายุก่อนหน้าหลังจากถูกตั้งค่าใหม่ให้หยุด

ใช้ no-store แทน max-age=0 โดยเจตนา Browser Cache TTL ของ zone จะเขียนทับ directive สำหรับ browser เมื่อใดก็ตามที่ค่าของ origin ต่ำกว่าการตั้งค่านั้น ดังนั้น max-age=0 จะปล่อยให้ edge เป็น ตามที่ zone กำหนด — สี่ชั่วโมงบน zone ที่ปล่อยไว้ตามค่าเริ่มต้น ซึ่งวัดเทียบกับ origin ที่ขอหกสิบวินาที การเพิ่ม PUBLIC_CACHE_MAX_AGE คือการเลือกใช้ edge cache และ การตั้งค่า zone นั้นสำหรับ browser: การ purge ตาม tag ล้าง Cloudflare ได้แต่ไม่มีทางล้าง browser ของใครได้ ดังนั้น ให้ตั้ง Browser Cache TTL ของ zone เป็น Respect Existing Headers ก่อนที่คุณจะพึ่งพามัน

จากตรงนี้ Settings → Data ใช้ import และ export เนื้อหา และ Generate standalone backend บนแดชบอร์ดคือเส้นทาง graduation การสร้างแบ็กเอนด์จะเขียนโปรเจกต์ออกมา; การ deploy มันคือส่วน ที่ต้องใช้บัญชี

rig นี้ใช้ backend/wrangler.dev.jsonc ซึ่งถูก track ใน git และไม่มี credential หรือ account id ใดๆ มันไม่เคยแตะ Cloudflare

  • บัญชี Cloudflare (Workers, D1, Pages และ R2 หรือ Cloudflare Images)
  • Node.js 22.14+ หรือ 24.10+ — เฉพาะสายเวอร์ชันเลขคู่เท่านั้น (repo มาพร้อม .nvmrc; nvm use จะเลือกให้)
  • npm ตรงตามที่ package.json#packageManager ประกาศไว้ (11.16.0) ซึ่งเป็น npm ที่มากับ Node ตาม .nvmrc ติดตั้งด้วย node scripts/install-dependencies.mjs ห้ามใช้ npm install หรือ npm ci เปล่าๆ: มันปฏิเสธ npm เวอร์ชันอื่น แตก locked tree โดยปิด script ไว้ ปฏิเสธ lifecycle script ของ dependency ทุกตัวที่ package.json#allowScripts ไม่ได้ตัดสินไว้ด้วยเวอร์ชันที่ตรงกันพอดี และหลังจากนั้นจึงรัน npm rebuild สำหรับตัวที่อนุมัติแล้ว
  • DeepInfra API key หากคุณต้องการใช้ฟีเจอร์ AI

Cloudflare Access (Zero Trust) ไม่ จำเป็น การตั้งค่าจะถามว่าจะเปิดใช้ เป็น perimeter เพิ่มเติมหรือไม่; การปฏิเสธ — ซึ่งเป็นค่าเริ่มต้น — จะใช้ session auth เป็น เส้นทางเข้าสู่ระบบ

clone แล้วรันตัวติดตั้ง (installer) สำหรับ admin CMS การส่งเนื้อหาไปยังเว็บไซต์สาธารณะต้องใช้ การตั้งค่าเพิ่มเติมในส่วนถัดไป

คู่มือการติดตั้งฉบับเต็ม ทั้งแบบเว็บและแบบบรรทัดคำสั่ง ดูที่ การติดตั้ง Dee Wan

Terminal window
git clone --recurse-submodules https://github.com/your-username/dee-wan-cms.git
cd dee-wan-cms
node scripts/install-dependencies.mjs
npm run setup # provisions D1 and media storage; offers DNS and optional Access
npm run deploy # builds and deploys the admin worker and admin UI
npm run health # checks the deployment answers

จากนั้น เปิดลิงก์ claim ที่ setup พิมพ์ออกมา แล้วตั้งรหัสผ่าน นั่นจะสร้าง superadmin และคุณก็เข้าสู่ระบบได้ ลิงก์ใช้ได้ครั้งเดียวและหมดอายุใน 15 นาที; หาก คุณพลาด ให้รัน npm run setup อีกครั้ง — ลิงก์ที่หมดอายุนับว่ายังไม่เสร็จ ดังนั้น การรันจะสร้างลิงก์ใหม่ให้ ตราบใดที่ยังไม่มีใคร claim installation

setup เสนอให้ apply schema ของฐานข้อมูลเป็นส่วนหนึ่งของการรัน (ค่าเริ่มต้นคือ yes) ดังนั้น การติดตั้งใหม่จึงไม่ต้องมีขั้นตอน migrate แยก

clone ใหม่ไม่ต้อง build ด้วยมือ packages/*/dist และ Prisma client เป็นผลลัพธ์ของการ build ดังนั้น clone จึงไม่มีทั้งสองอย่าง — และ setup, deploy และ migrate:d1 จะ build สิ่งที่ขาดก่อนที่จะทำอย่างอื่น

npm run doctor เป็นการวินิจฉัยแบบอ่านอย่างเดียวที่เลือกใช้ได้ บน clone ใหม่ มันจะรายงาน package ที่สร้างขึ้น และ Prisma client ที่ขาดหาย และออกด้วยค่าที่ไม่ใช่ศูนย์ ให้รันมันหลัง setup หรือ build ผลลัพธ์เหล่านั้น ก่อนด้วย npm run build:packages && npm run prisma:generate

เนื้อหาที่เผยแพร่ถูกให้บริการโดย public read worker แยกต่างหาก ซึ่งมี backend/wrangler.public.jsonc ของตัวเอง

npm run setup เขียนไฟล์นั้นให้คุณ มันทำเช่นนั้น ณ จุดที่ทุกค่าที่ต้องใช้ ลงตัวแล้ว — บัญชี, zone และ database id — และจะไม่เขียนทับไฟล์ที่มีอยู่แล้ว หาก ค่าใดค่าหนึ่งไม่สามารถหาได้ setup จะ ปฏิเสธโดยระบุชื่อ แทนที่จะเดา route หรือ database id และบอกไว้เช่นนั้น; นั่นคือกรณีที่คุณเตรียมมันด้วยมือ:

Terminal window
cp backend/wrangler.public.example.jsonc backend/wrangler.public.jsonc
# edit backend/wrangler.public.jsonc — account id, D1 id, route
npm run deploy

deploy ปฏิเสธที่จะรันเมื่อไม่มีไฟล์นั้น เพราะการ deploy ที่ไม่มี public worker ส่ง เนื้อหาที่เผยแพร่ไปยังที่ใดไม่ได้เลย และไม่ควรถูกเข้าใจผิดว่าเป็นการ deploy ที่เสร็จสมบูรณ์ installation ที่ ต้องการเพียง admin UI จริงๆ ให้ระบุไว้เช่นนั้น:

Terminal window
npm run deploy -- --admin-only

ซึ่งจะ deploy ฝั่งผู้ดูแล และรายงานการรันว่าเป็น admin-only แทนที่จะเป็นเสร็จสมบูรณ์

การ deploy installation สำรอง --config เลือก wrangler config ของ admin worker; public worker มี config ของตัวเอง และชื่อไฟล์หนึ่งไม่ได้บ่งบอกถึงอีกไฟล์หนึ่ง ดังนั้น --config ที่ไม่ใช่ค่าเริ่มต้น จึงต้องใช้ --public-config:

Terminal window
npm run deploy -- --config wrangler.v3.jsonc --public-config wrangler.v3.public.jsonc

หากไม่มีมัน การ deploy จะปฏิเสธ แทนที่จะจับคู่ admin worker สำรองของคุณกับ public worker ของ installation ค่าเริ่มต้น config ทั้งสองจะถูกอ่านและค่า account_id ถูกเปรียบเทียบกันก่อนที่จะมีสิ่งใด ถูก deploy

setup ต้องใช้ Cloudflare API token มันจะ ถามหา token หากคุณยังไม่ได้ตั้ง และพิมพ์รายการสิทธิ์ที่แน่นอนพร้อมเหตุผลของแต่ละข้อ:

Terminal window
export CLOUDFLARE_API_TOKEN=# optional — skips the prompt
export CLOUDFLARE_ACCOUNT_ID=# optional — skips the account picker

npm run doctor พิมพ์รายการเดียวกันในบรรทัดวิธีแก้เมื่อ token หายไป หรือมีขอบเขตสิทธิ์ไม่พอ ทั้งสองมาจากแหล่งเดียวกัน (scripts/cloudflare-token-scopes.ts) ดังนั้น token ที่สร้างตามที่ doctor ขอคือ token ที่ setup ยอมรับ

ไม่มีสิ่งอื่นที่ต้องใช้ environment variable secret ของแอปพลิเคชันถูกตั้งด้วย wrangler secret put (ด้านล่าง) ไม่เคยอยู่ในไฟล์

npm run setup เป็นแบบโต้ตอบ มันจัดเตรียมฐานข้อมูล D1 และ media bucket เสนอให้สร้าง DNS record ตั้งค่า Cloudflare Access application เฉพาะเมื่อคุณเลือกใช้ จากนั้นเขียน backend/wrangler.jsonc

wrangler.jsonc คือไฟล์ configuration — ไม่มี wrangler.toml

secret ไม่เคยถูกเก็บใน wrangler.jsonc หรือในฐานข้อมูล ตั้งค่าด้วย wrangler secret put จากไดเรกทอรี backend/:

Terminal window
npx wrangler secret put DEEPINFRA_API_KEY # AI text + image generation
npx wrangler secret put CF_IMAGES_API_TOKEN # only if STORAGE_PROVIDER=cf-images
npx wrangler secret put CF_PURGE_API_TOKEN # only if caching public reads; see PUBLIC_CACHE_MAX_AGE

DeepInfra เป็นผู้ให้บริการ inference รายเดียวที่รองรับ Chat, ภาพ และ engine สำหรับ structured generation ล้วนทำงานผ่านมันด้วย key เดียว admin UI ไม่เคย เรียกผู้ให้บริการโดยตรง — ทุกอย่างผ่าน /api/ai/* บน Worker ซึ่งถือ key และบังคับใช้ quota รายชั่วโมงต่อไซต์

การติดตั้งใหม่ไม่ต้องใช้สิ่งเหล่านี้ — setup apply schema ให้แล้ว คำสั่งเหล่านี้มีไว้สำหรับ ภายหลัง เมื่อ schema เปลี่ยน:

Terminal window
npm run migrate:d1 # apply pending migrations to the deployed database
npm run migrate:d1:local # …to the local mirror
npm run migrate:sync # regenerate backend/migrations-wrangler/ from Prisma
npm run migrate:check # verify the two are in step (CI)
npm run migrate:d1:local -- --create-migration # author one from schema.prisma

การ apply และการเขียน migration เป็นคนละเรื่องกัน หากไม่มี --create-migration คำสั่งเหล่านี้ จะ apply เฉพาะสิ่งที่มีอยู่แล้ว — จะไม่เขียน migration จาก schema.prisma ที่ drift ไป ซึ่งเป็นวิธีที่ DROP TABLE เข้าไปอยู่ในประวัติโดยไม่มีใครสังเกต

migration คือ Prisma migration ที่ถูก replay เข้าไปใน D1 จาก backend/migrations-wrangler/ ไม่มี schema.sql prisma/migrations/ คือ แหล่งความจริง และไดเรกทอรีของ wrangler ถูกสร้างจากมัน — เขียน Prisma migration แล้วรัน migrate:sync และ commit ทั้งสองส่วน

ทั้งสองอยู่ในส่วนผู้ดูแลใต้ Settings → Data — ไม่ต้องใช้ terminal ไม่ต้องแก้ JSON

  • เข้า: ไฟล์ archive ที่ export จาก Strapi (.tar / .tar.gz ตรวจสอบแล้วกับ Strapi 4.25.9 และ 5.51.1) หรือ Dee Wan site copy (.json) ที่ export จาก installation อื่นของ CMS นี้ wizard จะตรวจดู archive แสดงสิ่งที่พบ จับคู่ ประเภทและภาษา ตรวจสอบแผน แล้วจึงรัน ไม่มีสิ่งใดถูกเขียนทับ: การ import จะสร้างเนื้อหาหรือข้ามไป และสามารถย้อนกลับงานทั้งหมดได้
  • ออก: Download site copy เขียนไฟล์หนึ่งไฟล์ที่เก็บโมเดล ฟิลด์ ภาษา workflow การตั้งค่า เนื้อหาปัจจุบันของไซต์นี้ เวอร์ชันที่เผยแพร่ ในทุกที่ที่แตกต่างกัน และไบต์ที่แน่นอนของทุกภาพ รวมถึงสำเนาก่อนหน้า ที่เวอร์ชันที่ถูกนำติดมายังคงแสดงอยู่ เมื่อไบต์ใดไม่สามารถอ่านได้ อย่างแม่นยำจาก R2 หรือ Cloudflare Images การดาวน์โหลดจะถูกปฏิเสธโดยระบุชื่อ แทนที่จะ เขียนสำเนาที่ไม่ครบ ไฟล์นั้นคือสิ่งที่ importer อ่านกลับเข้ามา
  • Spreadsheet อัปเดตแถวที่มีอยู่แล้ว สิ่งนี้เริ่มจากรายการเนื้อหา ของโมเดลเดียว ไม่ใช่จาก wizard นี้

wizard ที่รันบนเครื่องกับ archive fixture ของ Strapi 5 (backend/test/fixtures/strapi/v5/archive/) เข้าไปยังไซต์ใหม่:

Import wizard ขั้น Source: ไฟล์ export ของ Strapi, Dee Wan site copy และแหล่งข้อมูลที่ถูกปิดใช้งานสองแหล่ง

Import wizard ขั้น Review: จำนวนที่พบใน archive และหนึ่งแถวต่อประเภทต้นทางที่จะสร้าง

Import wizard ขั้น Check: ผลการตัดสิน “Can import” เหนือรายการสิ่งที่ตรวจพบ

Import wizard ขั้น Run: Start importing, Pause และ Cancel ก่อนเริ่มการรัน

Import wizard ขั้น Done: จำนวนที่ import แล้ว ข้าม ล้มเหลว และไม่ได้รัน ต่อประเภทต้นทาง

การ import archive ต้องใช้ IMPORT_BUCKET npm run setup เสนอให้จัดเตรียมมัน และ npm run doctor รายงานว่าเปิดใช้งานอยู่หรือไม่ — ทุกอย่างอื่นทำงานได้ โดยไม่ต้องมีมัน

สถานะถูกระบุตามจริงใน SPEC-IMPORT.md: implement แล้วใน working tree แต่ยังไม่สมบูรณ์ตามระดับ 1.0

Terminal window
npm run local:api # admin Worker on http://localhost:8787, tracked dev config
npm run local:admin # admin UI on http://localhost:5173
npm run local:public # public read Worker on http://127.0.0.1:8788

local:api คือ wrangler dev -c wrangler.dev.jsonc — config สำหรับ local ที่ถูก track ซึ่ง clone ใหม่ มีอยู่แล้ว backend/wrangler.jsonc ถูกเขียนโดย setup และอยู่ใน gitignore ดังนั้นไม่มีสิ่งใดใน flow แบบ local ที่พึ่งพามัน; หากต้องการรัน configuration ที่ติดตั้งแล้วแทน ให้ใช้ npm run dev:install --workspace backend

local:admin ส่ง PUBLIC_BACKEND_URL ผ่าน command line ดังนั้น frontend จึงไม่ต้องมี .env สำหรับ เส้นทางนี้ การ deploy ยังคงอ่านมันจาก frontend/.env

Terminal window
npm run deploy # backend Worker + frontend Pages project (+ public worker when configured)
npm run health # post-deploy checks

health รันการตรวจสอบเดียวกันไม่ว่าจะมี Access หรือไม่; เมื่อเปิดใช้ Access มัน จะตรวจสอบ perimeter ด้วยโดยใช้คู่ service-token

setup พิมพ์ claim URL แบบใช้ครั้งเดียว ในตอนท้าย เปิดมันเพื่อตั้งรหัสผ่านและ สร้าง superadmin — ดูด้านล่าง

Cloudflare Access ไม่สร้างใครอีกต่อไปแล้ว มันยืนยันตัวตนผู้ใช้ที่มีอยู่ แล้ว (ซึ่งเป็นสิ่งที่ช่วงเวลา migration ต้องการ) และ Access identity ที่ไม่มี บัญชีจะได้ user_not_provisioned BOOTSTRAP_ADMIN_EMAIL ระบุที่อยู่อีเมล ที่ลิงก์ claim ผูกไว้; มันไม่ได้มอบสิทธิ์ใดๆ ด้วยตัวเอง

การลงทะเบียนปิดอยู่ ไม่มีการสมัครสมาชิกแบบสาธารณะ และไม่มีฟอร์ม “ลืมรหัสผ่าน” แบบสาธารณะ — บัญชีมีอยู่เพราะมีผู้ที่มีอำนาจสร้างมันขึ้น รหัสผ่านถูกตั้งโดยเจ้าของผ่านลิงก์แบบใช้ครั้งเดียว ไม่เคยถูกเลือกโดย ผู้ที่เป็นคนเชิญ

ทุกลิงก์ด้านล่าง ใช้ได้ครั้งเดียว หมดอายุใน 15 นาที และแสดงเพียง ครั้งเดียวเท่านั้น มีเพียง hash ของมันที่ถูกเก็บ ดังนั้นไม่มีสิ่งใดพิมพ์มันออกมาได้อีก; หากคุณทำหาย ให้ สร้างใหม่ ซึ่งจะทำให้ลิงก์แรกใช้ไม่ได้ ความยาวรหัสผ่านขั้นต่ำคือ 12 ตัวอักษร บังคับใช้โดยเซิร์ฟเวอร์

npm run setup พิมพ์ claim URL ในตอนท้ายของการติดตั้งใหม่ เปิดมัน ตั้ง รหัสผ่าน แล้วบัญชี superadmin จะถูกสร้าง ที่อยู่อีเมลของบัญชีมา จาก BOOTSTRAP_ADMIN_EMAIL ซึ่งต้องระบุที่อยู่เพียงหนึ่งที่อยู่เท่านั้น

หากลิงก์หมดอายุก่อนที่คุณจะใช้ ให้รันตัวติดตั้งอีกครั้ง (npm run setup หรือ npm run install:cms -- resume) ลิงก์ claim ที่หมดอายุนับว่ายังไม่เสร็จ ดังนั้นการรันจะสร้างลิงก์ใหม่ให้ ตราบใดที่ยังไม่มีใคร claim installation npm run claim:link ทำสิ่งนี้ไม่ได้: มันคืนบัญชี superadmin ที่มีอยู่แล้วให้กับ เจ้าของ และจะปฏิเสธเมื่อยังไม่มี superadmin อยู่เลย

superadmin สร้างลิงก์จากรายการ People และส่งมอบด้วยวิธีใดก็ได้ ที่ต้องการ — แชต โทรศัพท์ หรือพบกันตัวต่อตัว ไม่มีสิ่งใดถูกส่งทางอีเมล เพราะการติดตั้งค่าเริ่มต้น ไม่มีการตั้งค่าผู้ให้บริการอีเมล

นี่คือกรณีเดียวที่ไม่มีใครอื่นแก้ไขได้ จึงต้องใช้ credential สำหรับการ deploy:

Terminal window
npm run claim:link -- --remote --email locked-out@example.com

มันจะปฏิเสธ เว้นแต่ที่อยู่นั้นเป็น superadmin ที่ active เพียงคนเดียว หากมี สองคน อีกคนควรสร้างลิงก์จากรายการ People แทน — เส้นทางนั้น ไม่ต้องใช้สิทธิ์เข้าถึง Cloudflare เลย

Terminal window
cd backend && npm test # Vitest, against a local SQLite database
cd frontend && npm test # Vitest
cd frontend && npx svelte-check # type check
npm run submodules:check # fail if a submodule has uncommitted work
คำสั่ง สิ่งที่ทำ
npm run local:migrate Local เท่านั้น ไม่ต้องใช้ credential apply schema ไปยัง D1 mirror แบบ local ผ่าน backend/wrangler.dev.jsonc อ่าน --local ก่อนที่จะอ่าน credential ใดๆ ดังนั้นจึงรันได้บนเครื่องที่ไม่เคยเห็น Cloudflare token
npm run local:api admin Worker บน config สำหรับ local ที่ถูก track ไม่มีสิ่งใดที่มันแตะออกไปนอกเครื่อง
npm run local:public public read Worker บน D1 local เดียวกัน จาก config ชั่วคราวที่อยู่นอก repository ระบุไซต์ด้วย -H "Host: localhost"
npm run local:seed fixture rig บวก corpus ที่สมจริง แล้วพิมพ์ข้อมูลเข้าสู่ระบบ เป็น idempotent
npm run local:seed:fixture เฉพาะ fixture rig: ผู้ดูแลคนแรก หนึ่งไซต์ โมเดล Article draft สามรายการ และ Page ตัวอย่าง สิ่งที่ e2e spec ของ real-backend ใช้ตรวจสอบ
npm run local:seed:corpus เฉพาะ corpus: ไซต์สมมติสองไซต์ที่มีเนื้อหารูปแบบเหมือนจริง media คำแปล และ timestamp ย้อนหลัง ต้องมีผู้ดูแลของ fixture rig อยู่ก่อน
npm run local:admin admin UI ที่เชื่อมกับ Worker แบบ local โดยส่ง PUBLIC_BACKEND_URL ผ่าน command line
npm run test:boot เริ่ม Worker ทั้งสองภายใต้ workerd ที่ pin เวอร์ชันไว้ และล้มเหลวหากตัวใดตัวหนึ่งปฏิเสธ เป็นการตรวจสอบเดียวที่มองเห็น entrypoint export ที่ไม่ถูกต้อง
npm run doctor Preflight ไม่เปลี่ยนแปลงสิ่งใด ระบุสิ่งที่ขาดหายและวิธีแก้
npm run setup จัดเตรียมทรัพยากร Cloudflare เสนอให้ apply schema และพิมพ์ลิงก์ claim
npm run plan ผู้ดูแลระบบ (operator) อ่านอย่างเดียว พิมพ์ทุกขั้นตอนที่การติดตั้งจะทำ และสถานะที่แต่ละขั้นตอนอยู่แล้ว โดยหาทั้งหมดจาก config และ ledger บนดิสก์ ไม่แตะเครือข่าย ไม่สร้างสิ่งใด ไม่มีค่าใช้จ่าย เพิ่ม -- --json เพื่อรับแผนที่เครื่องอ่านได้ซึ่งมี planVersion
npm run install:cms operator เปลี่ยนแปลงสถานะ ตัวติดตั้ง (installer) ที่เรียกตามคำกริยา — install:cms plan, apply, resume, doctor, verify, teardown apply และ resume สร้างทรัพยากร Cloudflare ที่มีค่าใช้จ่าย; คำกริยาอื่นไม่สร้าง exit code แยก “ไม่สมบูรณ์” ออกจาก “เสีย” และจาก “ไม่มี credential” เพื่อให้ CI แตกแขนงตามได้
npm run install:web operator เปลี่ยนแปลงสถานะ ตัวติดตั้งเดียวกันที่มีหน้าจอบน browser ให้บริการจาก localhost บนเครื่องนี้ เพื่อให้ Cloudflare token อยู่ที่เดิมที่มันอยู่แล้ว ถูกล็อกไว้กับผู้เรียกรายเดียวด้วย secret แบบใช้ครั้งเดียว เป็น wizard สี่ขั้นตอน — Site (ไซต์จะอยู่ที่ไหน), Cloudflare (เชื่อมต่อ เลือกบัญชี แผนฐานข้อมูล D1), Administrator (ที่อยู่อีเมลเดียวที่ลิงก์ claim ผูกไว้), Install (ทบทวนที่อยู่ที่วางแผนไว้และสิ่งที่จะถูกสร้าง โดยชื่อโครงสร้างพื้นฐานอยู่หลังส่วนเปิดเผย “Customize infrastructure” หนึ่งส่วน) Continue บันทึกแต่ละขั้นตอน; โครงสร้างพื้นฐานสำหรับ import ปิดอยู่เว้นแต่จะเลือก เขียนสิ่งที่คุณตัดสินใจลงใน backend/wrangler.jsonc โดยไม่รบกวน comment ของไฟล์ ขอสิทธิ์เข้าถึงจาก Cloudflare เท่าที่การตั้งค่าเหล่านั้นต้องการพอดี และปฏิเสธที่จะเริ่มการรันกับการตั้งค่าที่คุณยังไม่ได้ทบทวน ขับเคลื่อน catalogue เดียวกับที่ plan พิมพ์
npm run install:web:dry-run ผู้ตรวจทาน ไม่เปลี่ยนแปลงสถานะ เซิร์ฟเวอร์และหน้าเดียวกับ install:web สำหรับการตรวจทาน UI/UX โดยแทนที่ผลกระทบภายนอกทุกอย่าง: config และ ledger อยู่ในโฟลเดอร์ชั่วคราวที่ถูกลบเมื่อกด Ctrl-C การค้นหาบัญชีและการรันการติดตั้งเป็นแบบ scripted และ “Create administrator” เปิดหน้าแจ้งเตือนแบบ local แทนที่อยู่ admin ที่ตั้งค่าไว้ การเข้าสู่ระบบ Cloudflare เสร็จสมบูรณ์เมื่อคุณเปิดลิงก์เข้าสู่ระบบที่พิมพ์ออกมาตอนเริ่ม (มันแสดงหน้า “Connected” จริง) หรือเสร็จเองด้วย --connect-ms <n>; อย่ากด Authorise ซึ่งจะเปิด Cloudflare ด้วย client id ที่เป็น placeholder การรันบันทึก ledger key จริง ดังนั้นแผนและ log จะเคลื่อนไปเหมือนในการติดตั้งจริง --outcome succeeded|incomplete|failed กำหนดผลเฉพาะการรันครั้งแรก (failed หยุดที่ health check, incomplete หยุดก่อนลิงก์ claim) และการ retry หรือ resume หลังจากนั้นจะสำเร็จ มี --accounts one|several|none|unreadable, --cloudflare oauth|unavailable, --step-ms, --credential-seconds ด้วย (60 หรือน้อยกว่าถือว่าหมดอายุทันที เพราะตัวติดตั้งทิ้ง credential ก่อนเวลา 60 วินาที) ใช้พอร์ต 8978 เว้นแต่จะตั้ง DEEWAN_INSTALL_PORT
npm run docs:api ผู้ร่วมพัฒนา ไฟล์ local เท่านั้น สร้าง docs/PUBLIC-API.md ใหม่จาก backend/src/public/api-docs.ts เขียนไฟล์เดียวใน repo และไม่มีอย่างอื่น; CI ตรวจสอบสำเนาที่ commit ไว้ แทนที่จะเขียนใหม่
npm run docs:plugins ผู้ร่วมพัฒนา ไฟล์ local เท่านั้น สร้าง docs/plugins/protocol-v1.md และ JSON Schema สองไฟล์ใต้ docs/plugins/schemas/ ใหม่จาก backend/src/lib/plugins/ เขียนสามไฟล์ใน repo และไม่มีอย่างอื่น backend/test/plugin-docs.test.ts ตรวจสอบสำเนาที่ commit ไว้ และรันทุกตัวอย่างในเอกสารปลั๊กอินกับ route จริง ดังนั้น drift จะทำให้เทสต์ล้มเหลว แทนที่จะทำให้ผู้เขียนปลั๊กอินเข้าใจผิด
npm run provision:site -- --site <id|slug> operator เปลี่ยนแปลงสถานะ ให้ไซต์หนึ่งมีฐานข้อมูล D1 ของตัวเอง ในแปดขั้นตอนที่บันทึกลง ledger: create → migrate → seed → bind → deploy → preflight → activate → smoke preflight ถามสิ่งที่ไม่ต้องพึ่ง router — Worker ที่ deploy แล้วทั้งสองตัว bind UUID ของฐานข้อมูลนี้พอดี ซึ่งอ่านจากบัญชีแทนที่จะอ่านจาก checkout นี้ และ tenant ตอบ query โดยตรงได้ — เพราะ registry.ts ปฏิเสธไซต์ที่ binding ไม่ได้อยู่ในสถานะ active และ request ที่ทำก่อน activation จะเป็นการวัดการปฏิเสธนั้น smoke ทำ request จริง เมื่อ activation ทำให้มันตอบได้แล้ว สร้างทรัพยากรที่มีค่าใช้จ่ายและ deploy Worker ทั้งสองใหม่ เพิ่ม --plan เพื่อพิมพ์ลำดับขั้นตอนและสิ่งที่ทำไปแล้ว — อ่านอย่างเดียว แต่มันจะอ่าน (READ) ฐานข้อมูลควบคุมผ่านเครือข่าย ต่างจาก npm run plan — หรือ --repair เพื่อตรวจสอบขั้นตอนที่ ledger ระบุว่าเสร็จแล้วอีกครั้ง; การ repair ไม่เคยรัน create, migrate หรือ seed ซ้ำกับ tenant ที่ให้บริการอยู่แล้ว และการ repair ที่ probe ไม่สามารถตอบได้จะปฏิเสธโดยไม่รันสิ่งใด แทนที่จะรายงานความสำเร็จจากแถวที่ไม่มีใครตรวจซ้ำ ไซต์ที่เคยจัดเตรียมเสร็จสมบูรณ์ครั้งหนึ่งจะถูกบันทึกไว้ถาวร และความล้มเหลวใดๆ ภายหลังจะลบฐานข้อมูลของมันไม่ได้ รันซ้ำสองครั้งได้อย่างปลอดภัย; argument ที่ไม่รู้จักจะถูกปฏิเสธ ตั้ง DEE_WAN_ADMIN_COOKIE เพื่อให้ smoke พิสูจน์การ route ฝั่งผู้ดูแลได้; หากไม่มี การรันจะหยุด แทนที่จะอ้างการตรวจสอบที่มันไม่ได้ทำ หากการรันก่อนหน้าล่มขณะถือ lease อยู่ ให้กู้คืนด้วย --declare-dead <owner> --declared-by <who> --evidence <what> — ครบทั้งสามส่วน เพราะการกู้คืนที่ไม่มีใครระบุที่มาได้คือการกู้คืนที่ไม่มีใครตรวจทานได้
curl -H "X-Site-Id: <id>" -b "<session cookie>" <admin>/api/models/public-client.ts สร้าง TypeScript client ที่ไม่มี dependency สำหรับ public read API ของไซต์นั้น — envelope, type ต่อโมเดล, locale union, query type ที่ key relation/tag เป็นฟิลด์ relation จริงของโมเดล และ method ที่ตั้งชื่อตามโมเดลของไซต์ อ่านอย่างเดียว โมเดลแบบ private ถูกรวมไว้โดย method ของมันต้องใช้ site API token; ฟิลด์ประเภทที่ไม่รู้จักถูกสร้างเป็น unknown ไม่เคยเป็น any ให้สร้างใหม่หลังเปลี่ยนโมเดล — ไม่มีสิ่งใดคอยเฝ้าดู drift
npm run acceptance -- plan พิมพ์เส้นทางการยอมรับสิบช่วง — doctor → plan → install → claim-link → provision-site → publish-read → public-query-matrix → tenant-upgrade → health → teardown-dry — พร้อมเกณฑ์ใน SPEC.md ที่แต่ละช่วงเป็นหลักฐานให้ ไม่รันสิ่งใดและไม่แตะบัญชีใด
npm run acceptance -- run --account <id> --confirm-disposable เจ้าของการ deploy เปลี่ยนแปลงสถานะ เฉพาะบัญชีที่ทิ้งได้ (DISPOSABLE) เท่านั้น สร้างทรัพยากรจริงที่มีค่าใช้จ่าย และ ทิ้งไว้ — ช่วงสุดท้ายคือ teardown แบบ dry run ดังนั้นการลบออกเป็นหน้าที่ของคุณในภายหลัง รันเส้นทางตามลำดับกับบัญชี Cloudflare จริง และเขียน ledger ไปที่ .dee-wan/acceptance-<account>.json: คำสั่ง exit code เวลา และเป้าหมายของทุกช่วง ledger ถูกผูกกับ TARGET FINGERPRINT ที่คำนวณจาก เนื้อหา ไม่ใช่ชื่อ: บัญชี, ไซต์สำหรับ acceptance, อีเมลผู้ดูแล, verification URL ทั้งสอง, digest ของทุก configuration ที่เส้นทางอ่าน (รวมถึง backend/wrangler.jsonc ซึ่ง provision:site และ tenant:upgrade อ่านไม่ว่า --config จะระบุอะไร และอยู่ใน gitignore จึงไม่มี path ใดบอกได้ว่ามีเนื้อหาอะไร), digest ของทุก blob ที่ stage ไว้, digest ของสิ่งที่ worktree มีเกินกว่า index และ pin ของ submodule แบบ recursive ที่แน่นอน การ resume ที่เป้าหมายเปลี่ยนไปจะถูก ปฏิเสธ พร้อมระบุฟิลด์ที่เปลี่ยน เพราะเก้าช่วงที่ผ่านบนเป้าหมายต่างกันสี่เป้าหมายไม่ได้อธิบายสถานะใดของโลกที่เคยทำงานได้จริง --restart ทิ้ง ledger ดังกล่าวและเริ่มเส้นทางใหม่; ต้องใช้ --allow-dirty เพื่อบันทึกหลักฐานจาก worktree ที่ไม่ตรงกับ commit ของตัวเอง หยุดที่ความล้มเหลวแรก — เส้นทางที่ดำเนินต่อหลังความล้มเหลวคือการวัดสถานะที่ไม่มีใครอธิบายไว้ ช่วงที่ขาด environment ที่จำเป็นจะถูกปฏิเสธ (REFUSED) ก่อนที่จะมีสิ่งใดถูกสร้าง และการรันจะไม่เขียน ledger เลย (NO ledger) — การรันที่ถูกปฏิเสธเคยบันทึกแถว skipped ที่ประทับด้วย environment ว่างเปล่าแบบเดียวกับที่มันเพิ่งปฏิเสธ ซึ่งการรันครั้งถัดไปที่ตั้งค่าถูกต้องอ่านเป็น drift การรันซ้ำจะทำช่วงที่ล้มเหลวซ้ำและข้ามช่วงที่ผ่านแล้ว; --from <leg> รันซ้ำจากช่วงนั้น และจะถูกปฏิเสธหากไม่ใช่ชื่อช่วง ทุกช่วงถูก pin ไว้กับบัญชีที่ระบุ ดังนั้นจึงไม่มีช่วงใดไปลงบัญชีอื่นได้ Exit 0 หมายความว่า LEDGER บันทึกทุกช่วงว่าผ่าน — ไม่ใช่ “ทุกอย่างที่การรันนี้พยายามทำสำเร็จ”; การรันบางส่วนออกด้วย 2 และระบุสิ่งที่ค้างอยู่ ต้องใช้ DEEWAN_ACCEPTANCE_ADMIN_EMAIL, DEEWAN_ACCEPTANCE_SITE_ID, DEE_WAN_ADMIN_COOKIE, ค่า VERIFY_* และ DEEWAN_PUBLIC_HOST / DEEWAN_PUBLIC_STRANGER_HOST สำหรับ filter matrix ตัวรันไม่ใช่หลักฐาน; ledger ที่มันทิ้งไว้คือหลักฐาน
npm run public:query-probe -- --confirm-disposable --site <id> --admin <url> --public <url> --host <domain> --stranger-host <domain> เจ้าของการ deploy เปลี่ยนแปลงสถานะ เฉพาะบัญชีที่ทิ้งได้ (DISPOSABLE) เท่านั้น filter matrix ของ R4.1: locale fallback, strict locale, relation equality, tag equality ผ่านเส้นทาง json_each ที่มาพร้อมผลิตภัณฑ์, การเดิน cursor หลายหน้า, การไม่รวมเนื้อหาที่ยังไม่เผยแพร่ และการแยก stranger-host แต่ละข้อตรวจสอบกับ ชุดผลลัพธ์ แทนที่จะเป็น status code และแต่ละข้อปฏิเสธการ hit edge cache ในฐานะหลักฐาน มันอ่าน locale policy ของไซต์ก่อน ยืมมาใช้สำหรับการตรวจแบบ strict และคืนค่า policy นั้นอย่างแม่นยำ — พร้อมลบทุกโมเดลและ instance ที่มันสร้าง — ใน finally ที่แต่ละขั้นตอนพยายามทำอย่างเป็นอิสระ ดังนั้นการลบที่ throw จะไม่หยุดการคืนค่า การสร้างที่สำเร็จแต่ body ไม่มี id จะถูกกู้คืนด้วย slug ที่ไม่ซ้ำของการรันเอง และจะรายงานเป็นทรัพยากรที่ตกค้างหากกู้คืนไม่ได้ มันไม่เคยสร้างภาษา: locale ที่สองต้องมีอยู่แล้ว ความล้มเหลวในการ cleanup หรือการคืนค่าจะไม่เขียน artifact ใดๆ มันยังอ่าน audit ledger แบบต่อท้ายอย่างเดียวของไซต์ (GET /api/sites/:id/audit ดังนั้น admin session ต้องมี audit:read) ก่อนอ่านไซต์และอีกครั้งหลัง cleanup และ artifact ระบุแถวที่การ apply โมเดล การลบโมเดล และการเขียนการตั้งค่าของมันเองทิ้งไว้ การเขียนเชิงผู้ดูแลอื่นใดที่แตะเป้าหมายที่การรันเปลี่ยน — การตั้งค่าของไซต์ ซึ่งการคืนค่าจะเขียนทับ หรือโมเดลที่มันสร้าง — จะทำให้การรันล้มเหลวโดยไม่มี artifact เช่นเดียวกับแถวที่คาดไว้ซึ่งหายไปหรือไม่เคยเสร็จสิ้น route ตอบเพียง 200 แถวดิบล่าสุดโดยไม่มี cursor ดังนั้นการรันที่ action แรกที่ถูก audit อยู่นอกช่วงนั้นจะปฏิเสธ แต่ละความล้มเหลวระบุประเภทของมัน (precondition, fixture, matrix, cleanup, audit) และความหมายต่อไซต์ ห้ามชี้มันไปที่ production, ของลูกค้า, ที่ใช้ร่วมกัน หรือโครงสร้างพื้นฐานอื่นที่ทิ้งไม่ได้ มันมีไว้สำหรับการ deploy ที่ทิ้งได้ซึ่งได้รับอนุญาตอย่างชัดแจ้งเท่านั้น — มันสร้าง เผยแพร่ และลบเนื้อหาจริง และเปลี่ยน locale policy ของไซต์ตลอดช่วงเวลาที่รัน ยังไม่เคยมีการรันเช่นนี้: ยังไม่มีหลักฐานภายนอกใดๆ
D1 and Workers analytics หลักฐานด้านความจุและประสิทธิภาพ (R4.2, R4.7, R4.9) ไม่มี harness ใน repository ที่วัดสิ่งเหล่านี้ หลังจากการ import ตามปกติและ traffic ตามปกติบนการ deploy ให้อ่านข้อมูลของ Cloudflare เอง: npx wrangler d1 insights <database> และ dataset ของ GraphQL Analytics API d1StorageAdaptiveGroups / d1AnalyticsAdaptiveGroups สำหรับขนาด D1, จำนวนแถว และ latency ของ query และ metric ของ Workers ในแดชบอร์ดสำหรับ CPU time และ latency ของ request ดู หลักฐานด้านประสิทธิภาพ (docs/PERFORMANCE-EVIDENCE.md, in the repository)
npm run acceptance -- report --account <id> พิมพ์ ledger นั้น พร้อมสิ่งที่แต่ละช่วงที่ผ่าน ไม่ได้ พิสูจน์ (NOT) ช่วงที่ไม่เคยรันจะระบุไว้เช่นนั้น แทนที่จะอ่านเหมือนไม่มีและไม่เป็นไร ไม่มีสิ่งใดในนี้ติ๊กเกณฑ์รีลีส — คนเป็นผู้อ่านและตัดสินใจ
npm run prisma:drift ตรวจสอบว่า Prisma schema ยังคงอธิบายสิ่งที่ migration สร้าง ตาราง SQL guard และตารางที่มีเฉพาะใน D1 ดิบมี allowlist ที่ระบุชัดเจน ความแตกต่างอื่นทุกอย่างคือ drift
npm run tenant:schema ผู้ร่วมพัฒนา ไฟล์ local เท่านั้น สร้าง backend/tenant-migrations/ ใหม่ ซึ่งเป็น schema ที่ฐานข้อมูลรายไซต์ที่เพิ่งสร้างได้รับ จาก migration ที่ถูก track และ lib/tenancy/placement.ts เขียนไฟล์เดียวใน repo
npm run tenant:schema:check ตรวจสอบว่า tenant schema ที่ check-in ไว้ยังตรงกับ migration (CI) migration ที่เพิ่มตารางจะทำให้สิ่งนี้ล้มเหลวจนกว่าจะมีคนกำหนดตำแหน่งให้มัน
npm run tenant:upgrade operator เปลี่ยนแปลงสถานะ apply tenant delta ที่ check-in ไว้ไปยังทุกฐานข้อมูลรายไซต์ที่ ACTIVE ซึ่งยังไม่ได้รับ โดยบันทึก ledger รายไซต์ผ่าน fanOutMigrations เพิ่ม --plan เพื่อถามแต่ละ tenant ว่าขาดอะไรโดยไม่เขียน, --site <id> สำหรับไซต์เดียว หรือ --declare-dead <owner> --declared-by <who> --evidence <what> — ลำดับสาม flag ที่แน่นอน — เพื่อกู้คืน lease ที่ runner ที่ตายไปแล้วถืออยู่ delta ledger ของแต่ละ tenant เอง (OWN) เป็นตัวตัดสินว่ามันมีอะไร ไม่ใช่ control ledger: delta ที่ไฟล์ถูกแก้ไขหลังจาก apply แล้วจะถูกปฏิเสธ, tenant ที่มี delta ซึ่ง build นี้ไม่ได้มาพร้อมจะถูกปฏิเสธ และ delta ที่บันทึกก่อนที่จะมี digest จะถูกรายงานเป็น unverifiable แทนที่จะเป็นอัปเดตแล้ว --site ที่ระบุ id ที่ไม่รู้จัก ไม่ active หรือไม่มี database id จะออกด้วยค่าที่ไม่ใช่ศูนย์ แทนที่จะรายงานว่าไม่มีอะไรต้องทำ ไฟล์ bootstrap ครอบคลุมฐานข้อมูลใหม่ (NEW); สิ่งนี้คือสิ่งที่นำฐานข้อมูลที่มีอยู่ขึ้นมาให้ทัน
npm run deploy build และ deploy admin worker และ UI รวมถึง public-read worker ที่จับคู่กัน --config เลือก admin wrangler config และ --public-config เลือกคู่ฝั่ง public; --config ที่ไม่ใช่ค่าเริ่มต้นต้องมีคู่ และจะไม่มีการ fallback ไปยัง public config ค่าเริ่มต้นเลย --admin-only deploy โดยไม่มี public worker และรายงานการรันเป็น admin-only --skip-migration-check ข้ามด่านตรวจ migration ที่ค้าง/ตรวจสอบไม่ได้ ซึ่งมิฉะนั้นจะปฏิเสธ
npm run health การตรวจสอบหลัง deploy กับ URL ที่ deploy แล้ว
npm run probe การตรวจสอบการเข้าถึงแบบไม่ระบุตัวตนและอ่านอย่างเดียว — ไม่มี credential ไม่แตะสิ่งใด
npm run verify ขับเคลื่อนการ deploy ที่ ทิ้งได้ ตั้งแต่ต้นจนจบ — สร้างไซต์ โมเดล และเนื้อหา ห้ามชี้ไปที่ production; ที่นั่นให้ใช้ --read-only
npm run migrate:d1 apply migration ที่ค้างไปยังฐานข้อมูลที่ deploy แล้ว (:local สำหรับ mirror)
npm run migrate:sync สร้าง wrangler migration mirror ใหม่จาก Prisma (migrate:check ใช้ตรวจสอบ)
npm run claim:link สร้างลิงก์ตั้งรหัสผ่านแบบใช้ครั้งเดียวสำหรับหนึ่งที่อยู่อีเมล
npm run revoke:sessions Break-glass: ออกจากระบบทุกคนใน deployment
npm run graduate สร้าง static artifact ของไซต์หนึ่งใหม่ (graduate:all สำหรับทุกไซต์) สิ่งที่ผู้รับไปดูแล (adopter) เป็นเจ้าของหลังจากนั้นและวิธีที่พวกเขาขยายมัน: การขยายแบ็กเอนด์ของคุณ
npm run graduate:all รันการ build artifact ของ graduation สำหรับทุกไซต์ ไม่ deploy สิ่งใด
npm run test:e2e รันชุด Cypress กับ dev server ที่มันเป็นเจ้าของเอง (frontend)
npm run teardown ลบสิ่งที่ setup จัดเตรียมไว้ เป็น dry run โดยค่าเริ่มต้น
npm run prisma:generate สร้าง Prisma client ใหม่หลังแก้ไข schema
npm run contract:embed ผู้ร่วมพัฒนา ไฟล์ local เท่านั้น สร้าง backend/src/lib/graduate/site-contract-embedded.ts ใหม่จาก backend/src/lib/page-contract/ generator ของ graduation ถูก bundle ไว้เพื่อไม่ให้อ่านสิ่งใดขณะรัน ดังนั้นไบต์เหล่านั้นจึงถูก commit เป็น string literal แทนที่จะถูก inline โดยขั้นตอน build — ซึ่งเป็นสิ่งที่ทำให้ bundle ที่ vendor ไว้สร้างซ้ำได้ด้วย npx esbuild ธรรมดา ให้รันหลังแก้ไขสิ่งใดใต้ page-contract/; มีเทสต์เปรียบเทียบทั้งสองแบบไบต์ต่อไบต์ ดังนั้น drift จะล้มเหลวแทนที่จะถูกส่งออกไปอย่างเงียบๆ
npm run submodules:sync กำหนดค่าเริ่มต้นหรืออัปเดต packages/ ที่ vendor ไว้
npm run submodules:check ล้มเหลวเมื่อ pin หรือ worktree ของ submodule drift ไป
npm run lint lint แบ็กเอนด์และ script ของตัวติดตั้ง บังคับใช้ใน CI
npm run typecheck:scripts typecheck scripts/ ซึ่งไม่มี config อื่นใดครอบคลุม
npm run verify -- --read-only --public <url> การตรวจสอบหลัง deploy ที่ไม่เขียนสิ่งใด เพิ่ม --model <slug> เพื่ออ่านโมเดลที่เผยแพร่แล้วหนึ่งโมเดล
npm run wire:github -- --repo <owner/name> [--project <dir>] เข้าสู่ระบบ GitHub สร้างหรือหา repo ปิดผนึก secret สามตัวที่ build workflow อ่าน และ push โปรเจกต์ที่ graduate แล้ว พิมพ์การตั้งค่า webhook ให้นำไปวาง
npm run exit -- --site <id> --repo <owner/name> --source <cms.db> --dest <dir> --db <target.db> ส่งมอบไซต์เป็น repository ที่ adopter เป็นเจ้าของ: graduate มันพร้อม lockfile ที่ commit แล้วและ snapshot ของข้อมูลที่เผยแพร่ commit และ push ตรวจสอบว่า branch บน remote ชี้ไปที่ commit นั้นจริง ปิดผนึก Cloudflare secret สั่ง deployment เฉพาะ repository ของ sha นั้นพอดี และรายงาน URL ของ repository, branch, commit, artifact id, Worker origin และ D1 id ไม่มีสิ่งใดไปถึง Cloudflare จนกว่า commit จะอ่านได้จาก remote เมื่อใช้ --media media ที่เผยแพร่จะย้ายไปยัง R2 ที่ adopter เป็นเจ้าของ (EXIT_MEDIA_DESTINATION_*) media ต้นทางที่อยู่ใน R2 ต้องใช้ EXIT_MEDIA_SOURCE_ACCOUNT_ID, EXIT_MEDIA_SOURCE_BUCKET, EXIT_MEDIA_SOURCE_ACCESS_KEY_ID และ EXIT_MEDIA_SOURCE_SECRET_ACCESS_KEY; media ต้นทางที่อยู่ใน Cloudflare Images ต้องใช้ EXIT_MEDIA_SOURCE_IMAGES_ACCOUNT_ID และ EXIT_MEDIA_SOURCE_IMAGES_API_TOKEN ต้องใช้เฉพาะกลุ่มที่ object ที่เผยแพร่ใช้เท่านั้น กลุ่มที่ขาดจะปฏิเสธก่อนการเขียนไปยังปลายทางใดๆ และไม่มี credential ใดถูกพิมพ์หรือเก็บไว้

npm run install:web:dry-run ทีละขั้นตอน — Site, Cloudflare, Administrator, Install — พร้อม บัญชี placeholder ที่มันใช้ตอบ:

ตัวติดตั้งแบบเว็บ ขั้นตอนเว็บไซต์: installation จะอยู่บนโดเมนของตัวเอง

ตัวติดตั้งแบบเว็บ ขั้นตอน Cloudflare: เชื่อมต่อสำหรับเซสชันนี้แล้ว เลือกแพ็กเกจ D1 แล้ว

ตัวติดตั้งแบบเว็บ ขั้นตอนติดตั้ง: การตรวจสอบที่อยู่ตามแผนและทุกสิ่งที่จะถูกสร้างขึ้น

ตัวติดตั้งแบบเว็บ เสร็จสิ้น: ทุกขั้นตอนเสร็จแล้วและปุ่มสร้างผู้ดูแลระบบ

ยินดีรับ PR มีสองธรรมเนียมที่ควรรู้:

  • packages/ เป็น submodule — commit และ push ที่นั่นก่อน แล้วจึงอัปเดต pointer
  • route ที่ผูกกับไซต์ต้องผ่านชั้น scoped CRUD ซึ่งใส่ site_id ให้ การข้ามชั้นนี้คือสาเหตุที่การอ่านข้าม tenant เกิดขึ้น

MIT Copyright (c) 2026 — ได้รับอนุญาตจากเจ้าของผลิตภัณฑ์เมื่อวันที่ 2026-08-27 พร้อม ข้อความฉบับเต็มใน LICENSE

สร้างในประเทศไทย 🇹🇭 สำหรับ edge ทั่วโลก

backend/wrangler.jsonc hardcode dee-wan-cms-backend, dee-wan-cms-db และ ชื่ออื่นๆ ทำนองเดียวกัน ดังนั้นการติดตั้งสองชุดในบัญชี Cloudflare เดียวจะชนกันด้วยชื่อ — และ teardown ที่เล็งผิดบัญชีจะลบฐานข้อมูลของอีก installation หนึ่ง

ตั้ง DEEWAN_RESOURCE_PREFIX เพื่อแยก namespace ของทุกสิ่งที่การรันจัดเตรียม:

Terminal window
DEEWAN_RESOURCE_PREFIX=test-a1b2 npm run setup # creates test-a1b2-dee-wan-cms-db, …
DEEWAN_RESOURCE_PREFIX=test-a1b2 npm run teardown # dry run; only touches test-a1b2-*

prefix ต้องเป็นตัวอักษรพิมพ์เล็ก ตัวเลข และขีด ไม่เกิน 24 ตัวอักษร เมื่อตั้งไว้ teardown จะ ข้ามและรายงาน ทรัพยากรใดก็ตามที่ชื่อไม่มี prefix นี้ ไม่ว่าไฟล์ config จะระบุอะไร — ดังนั้น config ที่เล็งผิดจะไปถึง ฐานข้อมูล production ไม่ได้ เมื่อไม่ได้ตั้ง พฤติกรรมจะเหมือนเดิมทุกประการ

ตั้งค่านี้สำหรับการติดตั้งใหม่ ไม่ใช่การติดตั้งที่มีอยู่แล้ว wrangler หาฐานข้อมูล D1 โดยค้นชื่อใน backend/wrangler.jsonc ดังนั้น prefix จะใช้ได้ ก็ต่อเมื่อ setup จัดเตรียมทรัพยากรและเขียนชื่อที่มี prefix ลงในไฟล์นั้นแล้วเท่านั้น การเปิดใช้กับการติดตั้งที่ config ยังมี ชื่อที่ไม่มี prefix จะทำให้ทุกคำสั่งล้มเหลวด้วย Couldn't find a D1 DB with the name or binding '<prefix>-…' หากต้องการย้าย การติดตั้งที่มีอยู่ไปใช้ prefix ให้รัน setup ใหม่; อย่าเพียงแค่ตั้งตัวแปร

สิ่งที่ตรวจสอบแล้วจนถึงตอนนี้: prefix ไปถึงทุก script ผ่าน loadConfig เป็น idempotent ถูกปฏิเสธเมื่อรูปแบบไม่ถูกต้อง และ teardown ปฏิเสธทรัพยากรที่มันไม่ได้ เป็นเจ้าของ (backend/test/teardown.test.ts) การเดินทางไปกลับเต็มรูปแบบ setup → Cloudflare → config ยังไม่ ถูกทดสอบ — นั่นต้องใช้บัญชีที่มี D1 และ R2

ชุดทดสอบอื่นทั้งหมดรันกับ harness แบบ local — Cypress stub admin API และ ขับ dev server ของ Vite ส่วนเทสต์ของแบ็กเอนด์รันบน SQLite แบบ local ไม่มีสิ่งใดมองเห็น ระบบที่ deploy แล้ว ซึ่งเป็นเหตุที่ /content/<id> กลายเป็นตอบด้วย API 404 ในขณะที่ e2e test ทั้ง 84 รายการยังคงเป็นสีเขียว

Terminal window
DEEWAN_SMOKE_PAGES=https://admin.example.com \
DEEWAN_SMOKE_HOST=https://cms.example.com \
npm run smoke --workspace=dee-wan-cms-backend

มันตรวจสอบสองสิ่งที่ความผิดพลาดในการเชื่อมต่อจะทำให้พังก่อน และไม่ต้องใช้ session:

  • Pages origin ให้บริการ SPA สำหรับ route แบบ dynamic ไม่ใช่แค่ / — deep link คือสิ่งแรกที่ _routes.json ที่ build ผิดทำให้พัง และเป็นสิ่งสุดท้าย ที่ใครจะตรวจสอบ เพราะแอปทำงานได้เสมอเมื่อคุณนำทางไปจากหน้าแรก
  • บนการติดตั้งที่เปิดใช้ Access /api/* บน hostname สาธารณะอยู่ หลัง Access — หาก route นั้นหรือ Access application ถูกลบออก perimeter ก็หายไป และไม่มีสิ่งอื่นใน repo นี้ตรวจสอบเรื่องนั้น บนการติดตั้งค่าเริ่มต้น (Access ปิด) API ได้รับการป้องกันด้วย session auth แทน และการตรวจสอบนี้ ไม่นำมาใช้

เป็น opt-in โดยเจตนา: เมื่อไม่ได้ระบุ origin มันจะข้ามแทนที่จะล้มเหลว ดังนั้นมัน จะไม่เสื่อมกลายเป็นชุดทดสอบสีแดงที่คนเรียนรู้ที่จะเพิกเฉย ให้รันหลังทุกการ deploy