Files
MechaCat02 813bc46640
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 39m53s
CI / Dashboard — check (push) Successful in 10m15s
docs(examples): add the CMS proof-of-concept dogfooding artifact
A WordPress-inspired CMS whose entire backend is PiCloud Rhai scripts
deployed with pic apply, plus a SvelteKit frontend. Built to dogfood the
project tool, CLI, and SDK; exercises docs/kv/files/users, docs+queue+cron
+pubsub triggers, the transactional-outbox notification chain, a docs
before-interceptor, set_if CAS, SSE, per-app CORS, env overlays, and a
durable Workflow (validate -> enrich || seo -> publish -> finalize) started
with workflow::start and polled with workflow::run_status, visualized live
with Svelte Flow.

FINDINGS.md / SECURITY.md capture every gap, surprise, and security issue
found (each tagged [PiCloud] vs [CMS]); the platform fixes for the
actionable ones ship in the preceding commit.

Also folds in the read-only `pic` allowlist entries added to
.claude/settings.json during the session (fewer-permission-prompts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:15:29 +02:00
..

PiCloud CMS PoC — a WordPress-inspired CMS on PiCloud

A proof-of-concept content-management system whose entire backend is PiCloud Rhai scripts, deployed declaratively with pic apply, plus a SvelteKit frontend. Built to dogfood the PiCloud project tool, CLI, and SDK — the gaps/surprises/issues found along the way are in FINDINGS.md and the security audit is in SECURITY.md.

This is a test artifact, not a production CMS. It deliberately exercises as many PiCloud features as fit a blog, and records where they helped or hurt.

What it does (WordPress-style)

Feature PiCloud pieces used
Admin dashboard + admin/author users users:: app-users + roles (admin/author/reader), a shared auth module as the access boundary
Post management + editor docs collection posts, kv slug index, kv::set_if CAS view counter
Page management + editor docs collection pages (one method-dispatched script on many routes)
Post comments + moderation docs collection comments, a docs before interceptor that HTML-escapes + sets moderation status
Reader users users:: register/login/verify, kv rate-limiter
Reader email on new posts docs trigger → durable queueemail::send_htmlMailpit
Post tags docs tag arrays + a tag-cloud endpoint
Scheduled publishing cron trigger flips scheduledpublished (which re-fires the notify chain)
Media uploads files collection (base64-in-JSON, image allowlist)
Live comments pubsub::publish_durablerealtime SSE (public topic) → browser EventSource
Editorial automation Workflow (durable DAG: validate → enrich ∥ seo → publish → finalize) started with workflow::start, polled with workflow::run_status; live-visualized with Svelte Flow in the admin dashboard
Config / secrets / envs vars (+ picloud.dev/prod.toml overlays), secrets (setup-token)
Deploy [project] + [app], declarative pic plan/apply --dir --env, --prune

Layout

picloud.toml            # the whole backend: scripts, routes, triggers, interceptor, vars, secrets
picloud.dev.toml        # dev env overlay      picloud.prod.toml  # prod env overlay
scripts/**/*.rhai       # ~30 Rhai scripts (endpoints, modules, triggers, interceptor)
run-server.sh           # boots picloud on :8081 against the picloud_cms DB, relaying mail to Mailpit
docker-compose.override.yml   # Mailpit
frontend/               # SvelteKit SPA (public site + admin dashboard)
FINDINGS.md  SECURITY.md

Run it

# 0. infra: Postgres (repo compose) + Mailpit
docker compose up -d postgres                       # from repo root
docker run -d --name cms-mailpit -p 127.0.0.1:1025:1025 -p 127.0.0.1:8025:8025 \
  -e MP_SMTP_AUTH_ACCEPT_ANY=true axllent/mailpit:latest
docker exec picloud-postgres-1 psql -U picloud -d picloud -c "CREATE DATABASE picloud_cms"

# 1. build + boot picloud (all-in-one) on :8081, relaying mail to Mailpit
cargo build -p picloud
bash examples/cms-poc/run-server.sh &                # see the script for the exact env

# 2. deploy the CMS (from examples/cms-poc/)
cd examples/cms-poc
pic login --url http://localhost:8081                # admin / admin
pic apps create cms --name "PiCloud CMS"             # apply can't create apps (FINDINGS F-006)
pic apps domains rm default <id> && pic apps domains add cms localhost   # claim the host (F-009)
echo -n "cms-setup-secret-123" | pic secrets set --app cms setup-token
pic apply --dir . --env dev

# 3. bootstrap the first admin (no CLI to grant app-user roles — FINDINGS F-012)
curl -X POST localhost:8081/cms/auth/bootstrap -H 'Content-Type: application/json' \
  -d '{"setup_token":"cms-setup-secret-123","email":"admin@cms.test","password":"adminpass1","display_name":"Site Admin"}'

# 4. frontend
cd frontend && npm install && npm run dev            # http://localhost:5173

Then: register a reader, write+publish a post as admin, watch the email land in Mailpit (http://localhost:8025), comment on the post, approve it, and see it appear live.

Backend API (all under /cms/, since /api/ is reserved — FINDINGS F-007)

  • Public: GET /cms/posts, GET /cms/posts/:slug, GET /cms/posts/:id/comments, POST /cms/posts/:id/comments, GET /cms/pages, GET /cms/pages/:slug, GET /cms/tags, GET /cms/media/:id, POST /cms/auth/{register,login,logout}, GET /cms/auth/me
  • Author+: GET/POST /cms/admin/posts, PUT/DELETE /cms/admin/posts/:id, POST /cms/admin/media
  • Admin: /cms/admin/pages*, /cms/admin/comments*, /cms/admin/users*

Headlines from the audit

  • 🟢 Strengths: the event model (docs/queue/cron/pubsub triggers), the transactional-outbox notification chain, set_if CAS, the docs before interceptor for write-time sanitization, users:: + roles, SSE, and the declarative plan/apply/--prune/--env loop all worked well.
  • 🟠 Rough edges: pic apply can't create an app (only groups); /api/ is a reserved route prefix; throw #{statusCode} becomes a 502 that leaks script internals; the docs filter DSL can't match array membership; user routes are JSON-only (no form/binary bodies); no CORS; no CLI to grant app-user roles.
  • 🔴 Security root cause: a public route holds full app authority and there's no per-row authz — every check is app code — while uncaught errors return a 502 that leaks the app id + script structure. Get one guard() wrong and it's a silent full bypass. See SECURITY.md.