@fabrika/builder 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD-CONTRACT.md +41 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/cli/src/bin.ts +66 -0
- package/cli/src/build-status.ts +109 -0
- package/cli/src/catalog.ts +42 -0
- package/cli/src/commands/advance.ts +73 -0
- package/cli/src/commands/build.ts +215 -0
- package/cli/src/commands/ensure-daemon.ts +25 -0
- package/cli/src/commands/gate.ts +165 -0
- package/cli/src/commands/induce.ts +61 -0
- package/cli/src/commands/init.ts +91 -0
- package/cli/src/commands/next.ts +31 -0
- package/cli/src/config.ts +47 -0
- package/cli/src/context.ts +28 -0
- package/cli/src/daemon-client.ts +280 -0
- package/cli/src/daemon-lifecycle.ts +171 -0
- package/cli/src/derive.ts +109 -0
- package/cli/src/index.ts +38 -0
- package/cli/src/induce.ts +151 -0
- package/cli/src/inject-skills.ts +313 -0
- package/cli/src/issues.ts +75 -0
- package/cli/src/layout.ts +124 -0
- package/cli/src/packet.ts +108 -0
- package/cli/src/phase-state.ts +19 -0
- package/cli/src/phases.ts +131 -0
- package/cli/src/skeletons.ts +104 -0
- package/daemon/src/bin.ts +70 -0
- package/daemon/src/build-stream.ts +38 -0
- package/daemon/src/handlers.ts +472 -0
- package/daemon/src/index.ts +123 -0
- package/daemon/src/paths.ts +30 -0
- package/daemon/src/phases.ts +221 -0
- package/daemon/src/static.ts +72 -0
- package/decisions/README.md +133 -0
- package/decisions/patterns/app-deploy.md +55 -0
- package/decisions/patterns/auth-password-kv.md +45 -0
- package/decisions/patterns/count-store.md +44 -0
- package/decisions/patterns/di-container.md +47 -0
- package/decisions/patterns/feature-architecture.md +48 -0
- package/decisions/patterns/lib-publish.md +46 -0
- package/decisions/patterns/monorepo.md +47 -0
- package/decisions/patterns/named-seeds.md +45 -0
- package/decisions/patterns/observability.md +45 -0
- package/decisions/patterns/single-worker-spa.md +74 -0
- package/decisions/patterns/structured-logging.md +49 -0
- package/decisions/patterns/typed-worker-rpc.md +87 -0
- package/decisions/patterns/vozka-app-deploy.md +74 -0
- package/decisions/patterns/write-split.md +47 -0
- package/decisions/technologies/analytics-engine.md +44 -0
- package/decisions/technologies/bindx.md +55 -0
- package/decisions/technologies/bun.md +51 -0
- package/decisions/technologies/buzola.md +80 -0
- package/decisions/technologies/cloudflare-access.md +50 -0
- package/decisions/technologies/cloudflare-vite-plugin.md +43 -0
- package/decisions/technologies/cloudflare.md +50 -0
- package/decisions/technologies/contember.md +65 -0
- package/decisions/technologies/cron-triggers.md +46 -0
- package/decisions/technologies/d1.md +51 -0
- package/decisions/technologies/durable-objects.md +44 -0
- package/decisions/technologies/kv.md +42 -0
- package/decisions/technologies/lopata.md +55 -0
- package/decisions/technologies/nuasite.md +50 -0
- package/decisions/technologies/oblaka.md +48 -0
- package/decisions/technologies/opice.md +40 -0
- package/decisions/technologies/pletivo.md +53 -0
- package/decisions/technologies/postgres.md +47 -0
- package/decisions/technologies/preact.md +44 -0
- package/decisions/technologies/propustka.md +137 -0
- package/decisions/technologies/queue.md +55 -0
- package/decisions/technologies/r2.md +41 -0
- package/decisions/technologies/react-router.md +47 -0
- package/decisions/technologies/react.md +37 -0
- package/decisions/technologies/roj.md +51 -0
- package/decisions/technologies/sentry.md +49 -0
- package/decisions/technologies/tailwind.md +45 -0
- package/decisions/technologies/vite.md +47 -0
- package/decisions/technologies/vozka.md +122 -0
- package/decisions/technologies/workers-ai.md +59 -0
- package/decisions/technologies/wrangler.md +41 -0
- package/decisions/tooling/deptective.md +45 -0
- package/decisions/tooling/edvabe.md +45 -0
- package/index.ts +2 -0
- package/package.json +51 -0
- package/skill/SKILL.md +76 -0
- package/skill/agents/build-orchestrator.md +170 -0
- package/skill/agents/flows-author.md +65 -0
- package/skill/agents/implementer.md +37 -0
- package/skill/agents/openai.yaml +4 -0
- package/skill/agents/opice-skeleton-author.md +68 -0
- package/skill/gates/brief.jsx +20 -0
- package/skill/gates/build.jsx +20 -0
- package/skill/gates/design.jsx +22 -0
- package/skill/gates/flows.jsx +14 -0
- package/skill/gates/review-grid.jsx +32 -0
- package/skill/gates/tech.jsx +17 -0
- package/skill/gates/vision.jsx +27 -0
- package/stacks/README.md +44 -0
- package/stacks/cf-fat.md +64 -0
- package/stacks/cf-internal-app.md +125 -0
- package/stacks/cf-lightweight.md +54 -0
- package/stacks/contember-app.md +53 -0
- package/stacks/content-site.md +45 -0
- package/ui/dist/assets/annotations-runtime.js +1 -0
- package/ui/dist/assets/client.css +2 -0
- package/ui/dist/assets/client.fhjppkvr.js +214 -0
- package/ui/dist/assets/components.js +63 -0
- package/ui/dist/assets/preact-compat.js +1 -0
- package/ui/dist/assets/react-jsx-dev-runtime.js +3 -0
- package/ui/dist/assets/react-jsx-runtime.js +3 -0
- package/ui/dist/assets/runtime.js +1 -0
- package/ui/dist/index.html +36 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: bun
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Bun (JS runtime + toolchain) — PREFERRED
|
|
5
|
+
slot: js-runtime
|
|
6
|
+
provides: [js-runtime, package-manager, test-runner, bundler, native-bundling]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [lopata]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "bun.sh/docs + bun (cloned source for internals)"
|
|
14
|
+
read: "The runtime/bundler/test-runner surface; `bun build` and bun plugins for the non-vite path."
|
|
15
|
+
- where: "Our repos that run on Bun: lopata, opice (`bun test`), pletivo"
|
|
16
|
+
read: "How our toolchain assumes Bun end to end."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Bun
|
|
20
|
+
|
|
21
|
+
> **Team preference.** We prefer **Bun + lopata + a non-vite stack**. Pick Bun as the
|
|
22
|
+
> runtime/toolchain and Bun-native bundling (e.g. `@buzola/bun-plugin`, pletivo's
|
|
23
|
+
> Bun build) wherever a decision offers it; reach for `vite` only when a chosen lib
|
|
24
|
+
> still mandates it (e.g. React Router v7).
|
|
25
|
+
|
|
26
|
+
## What it is
|
|
27
|
+
|
|
28
|
+
The JS runtime, package manager, test runner, and bundler our stack standardizes on.
|
|
29
|
+
Most of our libs assume Bun: `lopata` runs on it, `opice` runs under `bun test`,
|
|
30
|
+
`pletivo` builds with it.
|
|
31
|
+
|
|
32
|
+
## When & why to use
|
|
33
|
+
|
|
34
|
+
- Default for every fabrika app — one fast toolchain (install, run, test, build).
|
|
35
|
+
- Bun-native bundling avoids a separate vite pipeline (the **non-vite** preference).
|
|
36
|
+
|
|
37
|
+
## When NOT to use
|
|
38
|
+
|
|
39
|
+
- A dependency genuinely needs Node-only APIs Bun doesn't implement, or a lib hard-requires vite (then `vite` is a scoped exception, not the default).
|
|
40
|
+
|
|
41
|
+
## Impacts & gotchas
|
|
42
|
+
|
|
43
|
+
- `bun test` is the test runner (opice rides on it); `bunx` runs CLIs (oblaka, lopata, opice).
|
|
44
|
+
- Bun-native bundling differs from vite plugins — when a lib offers both (e.g. buzola), prefer the **bun-plugin**.
|
|
45
|
+
- Some Node libs assume Node built-ins; verify under Bun before depending on them.
|
|
46
|
+
|
|
47
|
+
## Relationships — why
|
|
48
|
+
|
|
49
|
+
- **slot `js-runtime`** — the runtime choice (vs Node).
|
|
50
|
+
- **recommends `lopata`** — the preferred local CF runtime, also Bun-based.
|
|
51
|
+
- Required/assumed by `pletivo`; `opice` runs under it.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: buzola
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Buzola — type-safe SPA router for React (Navigation API)
|
|
5
|
+
slot: router
|
|
6
|
+
provides: [router, spa, file-based-routing, type-safe-routes, loaders]
|
|
7
|
+
requires: [react]
|
|
8
|
+
recommends: [bun]
|
|
9
|
+
conflicts: [react-router]
|
|
10
|
+
npm: ["@buzola/router@^0.0.12", "@buzola/vite-plugin@^0.0.12", "@buzola/bun-plugin@^0.0.12", "@buzola/codegen@^0.0.12"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "buzola/packages/buzola/src/define/create-page.tsx + src/react/{hooks.ts,link.tsx}"
|
|
14
|
+
read: "The createPage() builder + the React surface (useNavigate/useParams/useRoute, Link, Outlet, BuzolaProvider)."
|
|
15
|
+
- where: "buzola/packages/{vite-plugin,bun-plugin,codegen}/src/"
|
|
16
|
+
read: "How codegen emits src/buzola.gen.ts + the virtual:buzola/routes module, wired via EITHER the vite-plugin OR the bun-plugin (the non-vite path)."
|
|
17
|
+
- where: "propustka/packages/admin-ui/src/{buzola.gen.ts,routes/principals/detail.tsx,routes/_layout.tsx}"
|
|
18
|
+
read: "A real app: codegen output, chained loaders + error boundary + type-safe Link, layout nesting."
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Buzola
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
A **type-safe SPA router for React** built over the browser **Navigation API**, with
|
|
26
|
+
**file-based routing** and **type codegen**. Routes are defined with a `createPage()`
|
|
27
|
+
builder; codegen emits `src/buzola.gen.ts` + a `virtual:buzola/routes` module and
|
|
28
|
+
augments a `BuzolaPageMap` so `navigate(pageId, params)` and `<Link to params>` are
|
|
29
|
+
fully typechecked. It is suitable for Contember-style admin SPAs, including Propustka's public admin UI.
|
|
30
|
+
Ships **both** a `@buzola/vite-plugin` and a **`@buzola/bun-plugin`** — the bun-plugin
|
|
31
|
+
is the **non-vite path** that fits our bun preference.
|
|
32
|
+
|
|
33
|
+
## When & why to use
|
|
34
|
+
|
|
35
|
+
- A client-rendered **SPA / admin panel** (auth-gated dashboards) where SSR isn't needed.
|
|
36
|
+
- You want **type-safe page ids + params** with zero manual route registration.
|
|
37
|
+
- Our **bun / non-vite** preference: drive codegen with `@buzola/bun-plugin` when
|
|
38
|
+
the app has no other reason for Vite. Use the Vite plugin for a client-rendered
|
|
39
|
+
two-process SPA/Worker stack.
|
|
40
|
+
|
|
41
|
+
## When NOT to use
|
|
42
|
+
|
|
43
|
+
- You need **SSR / data in loaders on the Worker** → `react-router`.
|
|
44
|
+
- You must support **Safari/Firefox or old browsers** — buzola **requires the Navigation API** (Chrome/Edge/Opera only); it throws at `BuzolaProvider` mount otherwise.
|
|
45
|
+
|
|
46
|
+
## Routing API (real shape)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export default createPage()
|
|
50
|
+
.params({ id: 'string' }) // typed params (or a Standard Schema)
|
|
51
|
+
.loader(async ({ params, redirect }) => { // chainable; results merge, first redirect wins
|
|
52
|
+
const user = await fetchUser(params.id)
|
|
53
|
+
if (!user) return redirect('login', {})
|
|
54
|
+
return { user }
|
|
55
|
+
})
|
|
56
|
+
.route('/principals/:id') // URL pattern — comes ONLY from .route(), not the filename
|
|
57
|
+
.render(({ params, data, invalidate, isLoading }) => (
|
|
58
|
+
<Detail user={data.user} />
|
|
59
|
+
))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Routes live in `src/routes/` (`_layout.tsx`, `_404.tsx`, `index.tsx`, `(group)/` are
|
|
63
|
+
special); the **page id** is the file path (`principals/detail.tsx` → `'principals/detail'`).
|
|
64
|
+
|
|
65
|
+
## Load-bearing gotchas (anti-staleness)
|
|
66
|
+
|
|
67
|
+
- **URL comes only from `.route()`**, never the filename — agents assuming Next-style file routing will get 404s. Check `buzola.gen.ts`.
|
|
68
|
+
- **`.render()` is required** — without it `createPage()` is incomplete and codegen drops the page.
|
|
69
|
+
- **Navigation API is mandatory** — no SSR, no old browsers; polyfill or use a memory adapter in tests.
|
|
70
|
+
- **Codegen needs a real module loader** — the vite-plugin/bun-plugin passes one (`ssrLoadModule` under vite); a bare Node `import()` silently loses routes.
|
|
71
|
+
- **Single `<Outlet/>` per layout** — no React-Router-style parallel/named outlets.
|
|
72
|
+
|
|
73
|
+
## Best practices (testability)
|
|
74
|
+
|
|
75
|
+
- Semantic markup + accessible names (`<Link>Dashboard</Link>`, `<label htmlFor>`, `role="alert"`) so opice selects by role/text, not brittle classes.
|
|
76
|
+
|
|
77
|
+
## Relationships — why
|
|
78
|
+
|
|
79
|
+
- **requires `react`** (it's a React router). Bundle via `@buzola/bun-plugin` (preferred, non-vite) or `@buzola/vite-plugin`; hence **recommends `bun`** rather than hard-requiring `vite`.
|
|
80
|
+
- **conflicts `react-router`** — same `router` slot; SPA (buzola) vs SSR (RRv7). One per app.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cloudflare-access
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare Access (legacy/alternate edge authentication)
|
|
5
|
+
slot: authentication
|
|
6
|
+
provides: [authentication, sso]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: []
|
|
9
|
+
conflicts: [auth-password-kv, propustka]
|
|
10
|
+
npm: ["jose@^5"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/cloudflare-one/policies/access (Access JWT, application audience, identity)"
|
|
14
|
+
read: "How Access fronts a Worker, the JWT it injects, and how to validate it (aud/iss, JWKS)."
|
|
15
|
+
- where: "A legacy app that still uses Access JWT validation"
|
|
16
|
+
read: "A live example of validating the Access JWT and resolving identity downstream; do not copy it into native Propustka apps."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Cloudflare Access (authentication)
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
Cloudflare's zero-trust **authentication** layer sitting in front of a Worker. It
|
|
24
|
+
handles "who you are" (SSO / IdP / one-time PIN), injects a signed JWT, and the
|
|
25
|
+
Worker validates it (via `jose` — `aud`, `iss`, JWKS). It owns _authentication
|
|
26
|
+
only_. This is now the legacy/alternate front door for apps that have **not**
|
|
27
|
+
adopted native Propustka sessions.
|
|
28
|
+
|
|
29
|
+
## When & why to use
|
|
30
|
+
|
|
31
|
+
- Self-hosted / internal tools where you want SSO without building login yourself.
|
|
32
|
+
- You intentionally keep an older Access-fronted app and are not migrating it to
|
|
33
|
+
native Propustka sessions yet.
|
|
34
|
+
|
|
35
|
+
## When NOT to use
|
|
36
|
+
|
|
37
|
+
- Public sign-up product (consumers can't be behind Access) → use `auth-password-kv` or an IdP-direct flow.
|
|
38
|
+
- You need app-owned credential lifecycle (password reset, invites) as a product feature.
|
|
39
|
+
|
|
40
|
+
## Impacts
|
|
41
|
+
|
|
42
|
+
- Production traffic must pass through Access; **local dev needs a dev-only
|
|
43
|
+
identity shim**.
|
|
44
|
+
- The Worker trusts the Access JWT — validate `aud`/`iss` against your application audience + team JWKS, never skip it.
|
|
45
|
+
|
|
46
|
+
## Relationships — why
|
|
47
|
+
|
|
48
|
+
- **conflicts `propustka`** — current Propustka owns native SSO sessions and
|
|
49
|
+
in-process path gates; it no longer expects a Cloudflare Access edge.
|
|
50
|
+
- **conflicts `auth-password-kv`** — both fill the `authentication` slot. Access = managed SSO; auth-password-kv = app-owned password+session. Pick one.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cloudflare-vite-plugin
|
|
3
|
+
kind: technology
|
|
4
|
+
title: "@cloudflare/vite-plugin (real-CF dev via workerd)"
|
|
5
|
+
slot: cf-dev-runtime
|
|
6
|
+
provides: [cf-dev-server, workerd-runtime, local-bindings]
|
|
7
|
+
requires: [cloudflare, vite, wrangler]
|
|
8
|
+
recommends: [oblaka]
|
|
9
|
+
conflicts: [lopata]
|
|
10
|
+
npm: ["@cloudflare/vite-plugin@^1", "wrangler@^4"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/workers/vite-plugin + installed @cloudflare/vite-plugin types"
|
|
14
|
+
read: "Plugin options and how it runs the Worker under workerd in dev, reading wrangler.jsonc bindings."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# @cloudflare/vite-plugin
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
Cloudflare's official vite plugin: runs the Worker in dev under **real workerd**
|
|
22
|
+
(miniflare), emulating D1/KV/R2/Queue/Cron locally from `wrangler.jsonc`. The
|
|
23
|
+
real-CF dev path (vs lopata's Bun-based emulation).
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- You use more/fresher CF primitives than lopata emulates (Queue, AE-adjacent, etc.) and want production-fidelity local behavior.
|
|
28
|
+
- The cf-fat path.
|
|
29
|
+
|
|
30
|
+
## When NOT to use
|
|
31
|
+
|
|
32
|
+
- You want the lightest local-first loop with the lopata dashboard and don't need workerd fidelity → use `lopata`.
|
|
33
|
+
|
|
34
|
+
## Impacts & gotchas
|
|
35
|
+
|
|
36
|
+
- `vite.config.ts`: `@cloudflare/vite-plugin` + the RRv7 plugin, SSR environment configured for Workers. Verify options against installed versions.
|
|
37
|
+
- Reads bindings from `wrangler.jsonc` (generated by `oblaka`); local D1 via `wrangler d1 … --local`.
|
|
38
|
+
- **`analytics-engine` is still not emulated** even here — counts go through `count-store`.
|
|
39
|
+
|
|
40
|
+
## Relationships — why
|
|
41
|
+
|
|
42
|
+
- **requires `cloudflare` + `vite` + `wrangler`** (workerd/miniflare + CLI).
|
|
43
|
+
- **conflicts `lopata`** — same `cf-dev-runtime` slot; pick one dev runtime (often env-toggled with `CF=1`). **`lopata` is our preferred default**; use this plugin when you specifically need workerd fidelity (e.g. Queue/Cron) lopata doesn't emulate yet.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cloudflare
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare Workers (platform)
|
|
5
|
+
slot: platform
|
|
6
|
+
provides: [platform, edge-compute, serverless]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [d1, durable-objects, kv, r2, queue, oblaka, wrangler]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["@cloudflare/workers-types@^4", "wrangler@^4"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/workers (fetch/queue/scheduled handlers, bindings, compatibility flags)"
|
|
14
|
+
read: "The Worker handler shapes (fetch/queue/scheduled), how bindings reach the handler via env, and compatibility flags (nodejs_compat)."
|
|
15
|
+
- where: "oblaka and lopata public examples"
|
|
16
|
+
read: "Resource definitions, local bindings, and multi-worker composition."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Cloudflare Workers
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
The serverless edge-compute platform fabrika's generated apps target: V8-isolate
|
|
24
|
+
Workers with first-party bindings to storage and messaging primitives
|
|
25
|
+
(`d1`, `kv`, `r2`, `queue`, `analytics-engine`, `durable-objects`) and
|
|
26
|
+
`cron-triggers` and Workers AI. Bindings arrive on `env`; a Worker exports
|
|
27
|
+
`fetch`, optionally `queue(batch, env)` and `scheduled(event, env)`.
|
|
28
|
+
|
|
29
|
+
## When & why to use
|
|
30
|
+
|
|
31
|
+
- The default platform for fabrika apps — SSR React + a database in one deployable unit, no cold-start servers.
|
|
32
|
+
- You want managed primitives (SQLite/KV/object-store/queue) without operating infrastructure.
|
|
33
|
+
|
|
34
|
+
## When NOT to use
|
|
35
|
+
|
|
36
|
+
- You need a long-lived process, raw TCP, or a runtime workerd/lopata can't emulate.
|
|
37
|
+
- Hard requirement on a non-CF managed service with no equivalent binding.
|
|
38
|
+
|
|
39
|
+
## Impacts
|
|
40
|
+
|
|
41
|
+
- App is a Worker: code runs in a V8 isolate (web APIs, `nodejs_compat` flag for selected Node built-ins) — **not** full Node.
|
|
42
|
+
- Bindings are defined in an oblaka resource graph, directly in `oblaka.ts` or
|
|
43
|
+
through `vozka.config.ts` for a Vozka-managed app; never hand-edit the generated
|
|
44
|
+
`wrangler.jsonc`.
|
|
45
|
+
- Local dev emulates D1/KV/R2/Queue/Cron (`lopata` or `wrangler dev`/workerd). **`analytics-engine` has no local runtime** (see `count-store`).
|
|
46
|
+
|
|
47
|
+
## Recommendations — why
|
|
48
|
+
|
|
49
|
+
- **d1 / durable-objects / kv / r2 / queue** — the storage/messaging menu; pick per workload (relational state → `d1`, coordination/strong-consistency → `durable-objects`, hot keys/sessions → `kv`, blobs → `r2`, buffering bursts → `queue`).
|
|
50
|
+
- **oblaka** — define bindings as code; **wrangler** — CLI for d1/dev/deploy.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: contember
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Contember — GraphQL CMS / backend framework
|
|
5
|
+
slot: backend
|
|
6
|
+
provides: [graphql-api, content-model, acl, admin-ui, migrations, tenant-auth, multi-tenant]
|
|
7
|
+
requires: [postgres]
|
|
8
|
+
recommends: [react, bindx]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: ["@contember/schema-definition", "@contember/react-binding", "@contember/interface", "@contember/client-content", "@contember/cli"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "contember-oss — packages/schema-definition (c.* DSL + @c.Allow ACL), CLAUDE.md, docker-compose.yaml"
|
|
14
|
+
read: "The schema-as-code DSL (entities/relations/validation), predicate-based ACL, the engine architecture (Content/System/Tenant API), and local Postgres bootstrap."
|
|
15
|
+
- where: "@contember/* on npm (installed types) + packages/client-content/tests/schema.ts"
|
|
16
|
+
read: "The package surface an app imports (schema-definition, react-binding, client-content, cli) + a real sample schema."
|
|
17
|
+
- where: "contember-oss/packages/playground/"
|
|
18
|
+
read: "A minimal working app: schema + CLI config + admin wiring."
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Contember
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
A **batteries-included GraphQL CMS/backend framework**. You declare the data model,
|
|
26
|
+
ACL, and validation as TypeScript decorators (`@contember/schema-definition`); the
|
|
27
|
+
engine auto-generates a GraphQL Content API (with row/field-level ACL enforced in
|
|
28
|
+
SQL), Postgres migrations (by diffing schema versions), a React admin UI, and a
|
|
29
|
+
tenant/identity system (sign-up, projects, memberships, roles, API keys). It is a
|
|
30
|
+
monolithic Node service speaking GraphQL over HTTP against Postgres — not a library
|
|
31
|
+
you compose, a stack you adopt.
|
|
32
|
+
|
|
33
|
+
## When & why to use
|
|
34
|
+
|
|
35
|
+
- You need a **working admin UI** + schema-as-code + Postgres migrations out of the box.
|
|
36
|
+
- **Row/field-level ACL** matters (author-only rows, role-hidden fields) — predicates baked into the query layer.
|
|
37
|
+
- **Multi-tenant + member roles** (per-project teams, invites) — the tenant API gives it free.
|
|
38
|
+
- You can run a **standalone Node backend** with a managed Postgres.
|
|
39
|
+
|
|
40
|
+
## When NOT to use
|
|
41
|
+
|
|
42
|
+
- You're committed to **Cloudflare Workers + D1** — Contember's engine is a Node service on Postgres; it does **not** run on Workers (this is the big architectural fork vs the cf-* stacks).
|
|
43
|
+
- You need a headless-only API with no React/admin → roll a thinner GraphQL layer yourself, or use `kvalt`.
|
|
44
|
+
- Bespoke SQL / aggregations the generated GraphQL fights.
|
|
45
|
+
|
|
46
|
+
## Impacts (what adopting it forces)
|
|
47
|
+
|
|
48
|
+
- **Postgres** as the relational store (so `d1` is out — see the `relational-db` slot). You run/manage Postgres.
|
|
49
|
+
- A **Node backend service** separate from any Workers SSR frontend — the deployment shape differs from cf-lightweight/cf-fat.
|
|
50
|
+
- **Schema is code**; migrations are diff-generated TypeScript committed under `.contember/migrations/`. Regenerate the snapshot whenever you add one or fresh DBs drift.
|
|
51
|
+
- A **projects/memberships** model (tenancy) even for single-tenant apps.
|
|
52
|
+
- The admin UI + data binding are **React 19** (see `bindx` / `@contember/react-binding`).
|
|
53
|
+
|
|
54
|
+
## Gotchas
|
|
55
|
+
|
|
56
|
+
- **ACL is per-field predicates**, declared on the entity (`@c.Allow(role, { when, read, … })`) and injected into SQL — not per-resolver. Complex scoping needs entity variables.
|
|
57
|
+
- **Tenant API owns identity** — custom auth (SAML, external JWT, machine principals) means bridging it, not swapping it.
|
|
58
|
+
- **No built-in realtime** — admin uses HTTP GraphQL; live updates need your own WS/polling.
|
|
59
|
+
- Migrations are diff-generated; hand-editing one (e.g. a data backfill) drops you into the migration format's internals.
|
|
60
|
+
|
|
61
|
+
## Relationships — why
|
|
62
|
+
|
|
63
|
+
- **requires `postgres`** — the entire schema/migration/ACL story is Postgres-specific (transitively excludes `d1`, same `relational-db` slot).
|
|
64
|
+
- **recommends `react` + `bindx`** — the admin UI and data binding are React; `bindx`/`@contember/react-binding` is the typed binding layer over its GraphQL API.
|
|
65
|
+
- **slot `backend`** — the backend choice (one backend per app). It also subsumes auth/identity, so it overlaps `auth-password-kv`/`propustka` conceptually — if Contember owns tenancy, don't double up.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: cron-triggers
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare Cron Triggers (scheduled jobs)
|
|
5
|
+
slot: null
|
|
6
|
+
provides: [scheduled-jobs, cron]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/workers/configuration/cron-triggers (scheduled handler, triggers.crons)"
|
|
14
|
+
read: "The scheduled(event, env) handler and triggers.crons config in wrangler."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Cloudflare Cron Triggers
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
Scheduled invocation of a Worker on a cron expression. The Worker exports
|
|
22
|
+
`scheduled(event, env)`; the schedule is `triggers.crons` in `wrangler.jsonc`. For
|
|
23
|
+
periodic work — spike/anomaly detection, digests, cleanup.
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- Periodic background work with no incoming request (alert spike detection, rollups, GC of expired sessions).
|
|
28
|
+
|
|
29
|
+
## When NOT to use
|
|
30
|
+
|
|
31
|
+
- Sub-minute or event-driven timing → use `queue` (event-driven) or a `durable-objects` alarm (precise per-object timer).
|
|
32
|
+
|
|
33
|
+
## Impacts & gotchas
|
|
34
|
+
|
|
35
|
+
- Export a **`scheduled(event, env)`** handler alongside `fetch`/`queue`. Emulated locally by workerd/lopata; trigger manually in dev.
|
|
36
|
+
- Schedule lives in `triggers.crons` — declare it via `oblaka.ts`, never hand-edit the generated `wrangler.jsonc`.
|
|
37
|
+
- Put the real operation in a reusable function and call it from both
|
|
38
|
+
`scheduled()` and a protected manual/internal endpoint used by local tests.
|
|
39
|
+
Do not make the scheduled handler call its own public HTTP endpoint.
|
|
40
|
+
- When several schedules share one Worker, branch on `event.cron` explicitly;
|
|
41
|
+
keep recovery/sweep operations idempotent because a trigger can overlap a
|
|
42
|
+
queue retry or a manual run.
|
|
43
|
+
|
|
44
|
+
## Relationships — why
|
|
45
|
+
|
|
46
|
+
- **requires `cloudflare`** — platform feature. **recommends `oblaka`** to declare the cron trigger.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: d1
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare D1 (relational database)
|
|
5
|
+
slot: relational-db
|
|
6
|
+
provides: [relational-db, sql, mutable-state]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka]
|
|
9
|
+
conflicts: [postgres]
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/d1 (bindings, migrations, query API)"
|
|
14
|
+
read: "The D1 binding API (prepare/bind/all/run), local migrations, and the execute CLI."
|
|
15
|
+
- where: "lopata + examples/react-router (local D1 via lopata d1)"
|
|
16
|
+
read: "How D1 is bound and queried in the canonical local-first RRv7-on-Workers example."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Cloudflare D1
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
SQLite-at-the-edge: the relational store for an app's **mutable, human-driven
|
|
24
|
+
state**. Bound as `env.DB` and queried inside the Worker (request/RPC handlers,
|
|
25
|
+
services, queue or scheduled handlers). SSR loaders/actions are one possible
|
|
26
|
+
transport; a Buzola SPA reaches D1 only through the Worker API. Migrations are
|
|
27
|
+
SQL files; local D1 lives under `.lopata/` (lopata) or `--local` (wrangler).
|
|
28
|
+
|
|
29
|
+
## When & why to use
|
|
30
|
+
|
|
31
|
+
- Default store for relational/mutable state (users, projects, config, issues…).
|
|
32
|
+
- You want SQL + local-first dev with no account.
|
|
33
|
+
|
|
34
|
+
## When NOT to use
|
|
35
|
+
|
|
36
|
+
- **High-frequency append/counter workload** (per-event count UPDATE = hot-row contention) → use `analytics-engine` behind `count-store`, append don't UPDATE.
|
|
37
|
+
- Coordination / strong single-object consistency → `durable-objects`.
|
|
38
|
+
- Large binary payloads → `r2`.
|
|
39
|
+
|
|
40
|
+
## Impacts & gotchas
|
|
41
|
+
|
|
42
|
+
- **Migrations are idempotent**: `migrations/NNNN_name.sql`, `CREATE TABLE IF NOT EXISTS`. Dir = oblaka `migrationsDir`.
|
|
43
|
+
- Keep D1 behind the Worker boundary. Inject `env.DB` into handlers/services;
|
|
44
|
+
never import a Worker binding into client SPA code.
|
|
45
|
+
- Apply: `lopata d1 migrations apply` (local-first) or `wrangler d1 migrations apply DB --local`.
|
|
46
|
+
|
|
47
|
+
## Relationships — why
|
|
48
|
+
|
|
49
|
+
- **requires `cloudflare`** — it's a CF binding.
|
|
50
|
+
- **recommends `oblaka`** — declare the `D1Database` binding in `oblaka.ts` rather than hand-editing `wrangler.jsonc`.
|
|
51
|
+
- **conflicts `postgres`** — same `relational-db` slot (edge SQLite vs a Postgres server). A `contember` stack uses Postgres, not D1.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: durable-objects
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare Durable Objects (coordination / strong consistency)
|
|
5
|
+
slot: null
|
|
6
|
+
provides: [coordination, stateful-objects, strong-consistency, websockets]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/durable-objects (class binding, storage API, alarms, WebSocket hibernation)"
|
|
14
|
+
read: "The DO class + binding model, transactional storage, alarms, and WebSocket hibernation."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Cloudflare Durable Objects
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
Single-instance stateful objects addressed by id, giving **strong single-object
|
|
22
|
+
consistency** and **coordination** — the thing D1/KV can't: a serialized point of
|
|
23
|
+
truth per entity. Each DO has transactional storage, alarms, and WebSocket support.
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- Per-entity coordination: rate limiters, locks, counters that must be exact, live presence.
|
|
28
|
+
- Real-time: WebSocket fan-out / rooms (hibernation keeps idle sockets cheap).
|
|
29
|
+
- A serialized actor where concurrent requests to the same id must not race.
|
|
30
|
+
|
|
31
|
+
## When NOT to use
|
|
32
|
+
|
|
33
|
+
- Plain relational/queryable state → `d1`. Hot cache/sessions → `kv`.
|
|
34
|
+
- High-cardinality append metrics (one DO per series doesn't scale) → `analytics-engine`.
|
|
35
|
+
|
|
36
|
+
## Impacts & gotchas
|
|
37
|
+
|
|
38
|
+
- Routed by id (`idFromName`/`idFromString`) — design the id space deliberately; all requests for one id serialize through one instance.
|
|
39
|
+
- Storage API is transactional but per-object; no cross-object queries.
|
|
40
|
+
- Local emulation under workerd/lopata; declare the class binding + migration in `oblaka.ts`.
|
|
41
|
+
|
|
42
|
+
## Relationships — why
|
|
43
|
+
|
|
44
|
+
- **requires `cloudflare`** — CF binding. **recommends `oblaka`** to declare the DO namespace/class.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: kv
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Cloudflare KV (key-value store)
|
|
5
|
+
slot: null
|
|
6
|
+
provides: [kv-store, sessions, dedup-cache]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka]
|
|
9
|
+
conflicts: []
|
|
10
|
+
npm: []
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "developers.cloudflare.com/kv (get/put/delete, TTL, eventual consistency)"
|
|
14
|
+
read: "The KV binding API and its eventual-consistency / TTL semantics."
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Cloudflare KV
|
|
18
|
+
|
|
19
|
+
## What it is
|
|
20
|
+
|
|
21
|
+
Eventually-consistent key-value store. Used for **session tokens**, **API
|
|
22
|
+
keys / DSN lookups**, and **dedup caches** — small values read on the hot path,
|
|
23
|
+
keyed by a stable id. Bound as `env.KV`.
|
|
24
|
+
|
|
25
|
+
## When & why to use
|
|
26
|
+
|
|
27
|
+
- Session storage (`auth-password-kv`), API-key validation on ingest, idempotency/dedup keys.
|
|
28
|
+
- Hot-path lookups where eventual consistency is acceptable.
|
|
29
|
+
|
|
30
|
+
## When NOT to use
|
|
31
|
+
|
|
32
|
+
- Read-after-write strong consistency required → `d1` or `durable-objects`.
|
|
33
|
+
- Relational queries / joins → `d1`.
|
|
34
|
+
|
|
35
|
+
## Impacts & gotchas
|
|
36
|
+
|
|
37
|
+
- **Eventually consistent** — a freshly written key may not be readable immediately everywhere; don't rely on it for read-after-write invariants.
|
|
38
|
+
- Values are opaque blobs (string/ArrayBuffer); model structure yourself. Use TTL for ephemeral session/dedup keys.
|
|
39
|
+
|
|
40
|
+
## Relationships — why
|
|
41
|
+
|
|
42
|
+
- **requires `cloudflare`** — CF binding. **recommends `oblaka`** — declare `KVNamespace` in `oblaka.ts`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: lopata
|
|
3
|
+
kind: technology
|
|
4
|
+
title: lopata (local Cloudflare runtime, local-first)
|
|
5
|
+
slot: cf-dev-runtime
|
|
6
|
+
provides: [local-cf-runtime, cf-dev-server, local-d1, local-bindings]
|
|
7
|
+
requires: [cloudflare]
|
|
8
|
+
recommends: [oblaka, bun, vite]
|
|
9
|
+
conflicts: [cloudflare-vite-plugin]
|
|
10
|
+
npm: ["lopata@^0.18.4"]
|
|
11
|
+
status: wip
|
|
12
|
+
sources:
|
|
13
|
+
- where: "lopata (repo) + installed CLI/plugin source"
|
|
14
|
+
read: "Worker entry, standalone dev command, optional vite-plugin wiring and the d1 CLI."
|
|
15
|
+
- where: "lopata examples"
|
|
16
|
+
read: "Vite integration, resource config, development commands, and multi-worker service bindings."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# lopata
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A drop-in CF runtime for **local dev** that runs on Bun (not workerd). Bindings are
|
|
24
|
+
backed by SQLite + FS under `.lopata/`, with a dashboard at `/__dashboard`. Lets
|
|
25
|
+
the whole stack run locally with **no CF credentials**.
|
|
26
|
+
|
|
27
|
+
## When & why to use
|
|
28
|
+
|
|
29
|
+
- Local-first development: D1/KV/R2 emulated on disk, zero account.
|
|
30
|
+
- The cf-lightweight path; `bunx lopata d1 …` for migrations/seeds/queries.
|
|
31
|
+
|
|
32
|
+
## When NOT to use
|
|
33
|
+
|
|
34
|
+
- You need the **real workerd** behavior or a binding lopata doesn't emulate yet → use `cloudflare-vite-plugin` (workerd) instead. lopata is **WIP — expect churn**, so verify what it emulates.
|
|
35
|
+
|
|
36
|
+
## Impacts & gotchas
|
|
37
|
+
|
|
38
|
+
- **Buzola SPA / Worker split:** run lopata as the standalone Worker dev server;
|
|
39
|
+
run Vite separately and proxy only `/rpc`/`/api`/`/internal` to it. No lopata
|
|
40
|
+
Vite plugin is needed in this shape.
|
|
41
|
+
- **Integrated SSR shape:** when using `lopata/vite-plugin`, verify its current
|
|
42
|
+
`viteEnvironment`/config options. The `'ssr'` environment is specific to the
|
|
43
|
+
SSR-on-Workers path, not a requirement for a client-only SPA.
|
|
44
|
+
- `CF=1` can swap lopata → `@cloudflare/vite-plugin` (real workerd) in stacks that
|
|
45
|
+
explicitly implement that seam. The two share the `cf-dev-runtime` slot.
|
|
46
|
+
- Local D1 under `.lopata/d1/*.sqlite`; CLI `bunx lopata d1 migrations apply` / `execute DB --file …`.
|
|
47
|
+
- Generate `wrangler.jsonc` from oblaka/the shared Vozka resource builder before
|
|
48
|
+
starting lopata. Never maintain local bindings in a second hand-written config.
|
|
49
|
+
|
|
50
|
+
## Relationships — why
|
|
51
|
+
|
|
52
|
+
- **requires `cloudflare`** (it emulates CF). Vite is recommended, not required:
|
|
53
|
+
lopata can run the Worker separately from a Vite SPA.
|
|
54
|
+
- **conflicts `cloudflare-vite-plugin`** — same `cf-dev-runtime` slot; they're swappable dev paths (env-toggled), not used together. **lopata is our preferred default** (local-first, Bun); reach for the cloudflare-vite-plugin only when you need workerd fidelity.
|
|
55
|
+
- **recommends `bun`** — lopata is Bun-based; part of our bun/lopata preference.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: nuasite
|
|
3
|
+
kind: technology
|
|
4
|
+
title: Nua Site — Astro meta-package + visual CMS
|
|
5
|
+
slot: site-framework
|
|
6
|
+
provides: [astro-site, visual-cms, markdown, seo-checks, sitemap]
|
|
7
|
+
requires: []
|
|
8
|
+
recommends: [tailwind, cloudflare]
|
|
9
|
+
conflicts: [pletivo]
|
|
10
|
+
npm: ["@nuasite/nua@^0.43", "@nuasite/cms@^0.43", "@nuasite/cli@^0.43", "@nuasite/components@^0.43", "@nuasite/checks@^0.43"]
|
|
11
|
+
status: stable
|
|
12
|
+
sources:
|
|
13
|
+
- where: "nuasite/README.md + packages/nua/README.md"
|
|
14
|
+
read: "The meta-package (pins Astro 6 + Tailwind 4 + MDX + sitemap) and how to configure it."
|
|
15
|
+
- where: "nuasite/packages/cms/README.md"
|
|
16
|
+
read: "The visual editor: how it injects data-cms-id markers and writes edits back to .astro source via HMR."
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Nua Site
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A published set of **Astro meta-packages** for building **content sites with a visual
|
|
24
|
+
CMS**. `@nuasite/nua` pins the toolchain (Astro 6, Tailwind 4, MDX, sitemap);
|
|
25
|
+
`@nuasite/cms` is an Astro integration that serves a visual editor in dev and writes
|
|
26
|
+
edits straight back to `.astro` source files; `@nuasite/checks` validates SEO/a11y/perf
|
|
27
|
+
at build. It mirrors the hosted Nua Site service so local builds match production.
|
|
28
|
+
|
|
29
|
+
## When & why to use
|
|
30
|
+
|
|
31
|
+
- A **marketing/docs/blog site** that needs **in-place visual editing** with edits flowing back to Git-tracked source.
|
|
32
|
+
- You want a validated Astro toolchain + LLM-friendly content export (`.md` endpoints, `AGENTS.md`).
|
|
33
|
+
|
|
34
|
+
## When NOT to use
|
|
35
|
+
|
|
36
|
+
- A dynamic **app** (SPA/SSR) → `react-router` / `buzola`.
|
|
37
|
+
- You want the fastest minimal SSG with no CMS → `pletivo`.
|
|
38
|
+
- **Note vs our bun/non-vite preference:** Astro is vite-based, so nuasite leans on vite under the hood — fine for content sites, but it's not the bun-native path.
|
|
39
|
+
|
|
40
|
+
## Impacts & gotchas
|
|
41
|
+
|
|
42
|
+
- Pages must be **`.astro`** for CMS editing (React/TSX components are islands, not CMS-marked).
|
|
43
|
+
- **Locked dependency versions** (the meta-package pins Astro/Tailwind) — you upgrade when Nua Site releases.
|
|
44
|
+
- The CMS editor parses **Tailwind `class="…"`** (style via utilities, not inline) and has **no built-in auth** — front it with `cloudflare-access` if edits must be gated.
|
|
45
|
+
- (Optional) `@pletivo/astro-jsx-pages` / `astro-jsx-pages` lets you author pages in `.tsx` instead of `.astro` — pre-release; not for the CMS-editing workflow.
|
|
46
|
+
|
|
47
|
+
## Relationships — why
|
|
48
|
+
|
|
49
|
+
- **conflicts `pletivo`** — same `site-framework` slot.
|
|
50
|
+
- **recommends `tailwind`** (the CMS depends on utility classes) + **`cloudflare`** (a common deploy target).
|