@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.
Files changed (112) hide show
  1. package/BUILD-CONTRACT.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +100 -0
  4. package/cli/src/bin.ts +66 -0
  5. package/cli/src/build-status.ts +109 -0
  6. package/cli/src/catalog.ts +42 -0
  7. package/cli/src/commands/advance.ts +73 -0
  8. package/cli/src/commands/build.ts +215 -0
  9. package/cli/src/commands/ensure-daemon.ts +25 -0
  10. package/cli/src/commands/gate.ts +165 -0
  11. package/cli/src/commands/induce.ts +61 -0
  12. package/cli/src/commands/init.ts +91 -0
  13. package/cli/src/commands/next.ts +31 -0
  14. package/cli/src/config.ts +47 -0
  15. package/cli/src/context.ts +28 -0
  16. package/cli/src/daemon-client.ts +280 -0
  17. package/cli/src/daemon-lifecycle.ts +171 -0
  18. package/cli/src/derive.ts +109 -0
  19. package/cli/src/index.ts +38 -0
  20. package/cli/src/induce.ts +151 -0
  21. package/cli/src/inject-skills.ts +313 -0
  22. package/cli/src/issues.ts +75 -0
  23. package/cli/src/layout.ts +124 -0
  24. package/cli/src/packet.ts +108 -0
  25. package/cli/src/phase-state.ts +19 -0
  26. package/cli/src/phases.ts +131 -0
  27. package/cli/src/skeletons.ts +104 -0
  28. package/daemon/src/bin.ts +70 -0
  29. package/daemon/src/build-stream.ts +38 -0
  30. package/daemon/src/handlers.ts +472 -0
  31. package/daemon/src/index.ts +123 -0
  32. package/daemon/src/paths.ts +30 -0
  33. package/daemon/src/phases.ts +221 -0
  34. package/daemon/src/static.ts +72 -0
  35. package/decisions/README.md +133 -0
  36. package/decisions/patterns/app-deploy.md +55 -0
  37. package/decisions/patterns/auth-password-kv.md +45 -0
  38. package/decisions/patterns/count-store.md +44 -0
  39. package/decisions/patterns/di-container.md +47 -0
  40. package/decisions/patterns/feature-architecture.md +48 -0
  41. package/decisions/patterns/lib-publish.md +46 -0
  42. package/decisions/patterns/monorepo.md +47 -0
  43. package/decisions/patterns/named-seeds.md +45 -0
  44. package/decisions/patterns/observability.md +45 -0
  45. package/decisions/patterns/single-worker-spa.md +74 -0
  46. package/decisions/patterns/structured-logging.md +49 -0
  47. package/decisions/patterns/typed-worker-rpc.md +87 -0
  48. package/decisions/patterns/vozka-app-deploy.md +74 -0
  49. package/decisions/patterns/write-split.md +47 -0
  50. package/decisions/technologies/analytics-engine.md +44 -0
  51. package/decisions/technologies/bindx.md +55 -0
  52. package/decisions/technologies/bun.md +51 -0
  53. package/decisions/technologies/buzola.md +80 -0
  54. package/decisions/technologies/cloudflare-access.md +50 -0
  55. package/decisions/technologies/cloudflare-vite-plugin.md +43 -0
  56. package/decisions/technologies/cloudflare.md +50 -0
  57. package/decisions/technologies/contember.md +65 -0
  58. package/decisions/technologies/cron-triggers.md +46 -0
  59. package/decisions/technologies/d1.md +51 -0
  60. package/decisions/technologies/durable-objects.md +44 -0
  61. package/decisions/technologies/kv.md +42 -0
  62. package/decisions/technologies/lopata.md +55 -0
  63. package/decisions/technologies/nuasite.md +50 -0
  64. package/decisions/technologies/oblaka.md +48 -0
  65. package/decisions/technologies/opice.md +40 -0
  66. package/decisions/technologies/pletivo.md +53 -0
  67. package/decisions/technologies/postgres.md +47 -0
  68. package/decisions/technologies/preact.md +44 -0
  69. package/decisions/technologies/propustka.md +137 -0
  70. package/decisions/technologies/queue.md +55 -0
  71. package/decisions/technologies/r2.md +41 -0
  72. package/decisions/technologies/react-router.md +47 -0
  73. package/decisions/technologies/react.md +37 -0
  74. package/decisions/technologies/roj.md +51 -0
  75. package/decisions/technologies/sentry.md +49 -0
  76. package/decisions/technologies/tailwind.md +45 -0
  77. package/decisions/technologies/vite.md +47 -0
  78. package/decisions/technologies/vozka.md +122 -0
  79. package/decisions/technologies/workers-ai.md +59 -0
  80. package/decisions/technologies/wrangler.md +41 -0
  81. package/decisions/tooling/deptective.md +45 -0
  82. package/decisions/tooling/edvabe.md +45 -0
  83. package/index.ts +2 -0
  84. package/package.json +51 -0
  85. package/skill/SKILL.md +76 -0
  86. package/skill/agents/build-orchestrator.md +170 -0
  87. package/skill/agents/flows-author.md +65 -0
  88. package/skill/agents/implementer.md +37 -0
  89. package/skill/agents/openai.yaml +4 -0
  90. package/skill/agents/opice-skeleton-author.md +68 -0
  91. package/skill/gates/brief.jsx +20 -0
  92. package/skill/gates/build.jsx +20 -0
  93. package/skill/gates/design.jsx +22 -0
  94. package/skill/gates/flows.jsx +14 -0
  95. package/skill/gates/review-grid.jsx +32 -0
  96. package/skill/gates/tech.jsx +17 -0
  97. package/skill/gates/vision.jsx +27 -0
  98. package/stacks/README.md +44 -0
  99. package/stacks/cf-fat.md +64 -0
  100. package/stacks/cf-internal-app.md +125 -0
  101. package/stacks/cf-lightweight.md +54 -0
  102. package/stacks/contember-app.md +53 -0
  103. package/stacks/content-site.md +45 -0
  104. package/ui/dist/assets/annotations-runtime.js +1 -0
  105. package/ui/dist/assets/client.css +2 -0
  106. package/ui/dist/assets/client.fhjppkvr.js +214 -0
  107. package/ui/dist/assets/components.js +63 -0
  108. package/ui/dist/assets/preact-compat.js +1 -0
  109. package/ui/dist/assets/react-jsx-dev-runtime.js +3 -0
  110. package/ui/dist/assets/react-jsx-runtime.js +3 -0
  111. package/ui/dist/assets/runtime.js +1 -0
  112. package/ui/dist/index.html +36 -0
@@ -0,0 +1,46 @@
1
+ ---
2
+ id: lib-publish
3
+ kind: pattern
4
+ title: Lib publish — tag → npm publish with OIDC trusted publishing + provenance
5
+ slot: null
6
+ provides: [npm-publish, oidc-trusted-publish, provenance]
7
+ requires: []
8
+ recommends: [bun, monorepo]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "oblaka / lopata / buzola / pletivo — .github/workflows/release.yml (tag-triggered publish)"
13
+ read: "The `on: push: tags: ['v*']` trigger, `permissions: id-token: write`, and `npm publish --provenance --access public` with no NPM_TOKEN."
14
+ - where: "opice — .github/workflows/release.yml (monorepo: version rewrite + per-package publish); roj — .github/workflows/publish.yml + scripts/npm-publish/"
15
+ read: "Multi-package coordination: rewrite versions + resolve workspace:* refs at publish time."
16
+ ---
17
+
18
+ # Lib publish (npm, OIDC trusted publishing)
19
+
20
+ ## What it is
21
+
22
+ A library publishes on a **git tag `v*`** via GitHub Actions using **OIDC trusted
23
+ publishing** — `permissions: id-token: write`, then **`npm publish --provenance
24
+ --access public`** with **no long-lived `NPM_TOKEN`** (GitHub's OIDC token is
25
+ exchanged by npm for short-lived registry auth, and `--provenance` attaches SLSA
26
+ build attestation). Requires **npm ≥ 11.5.1** (some workflows `npm i -g npm@latest`
27
+ first). Version bumps are explicit (`package.json` → tag), no changeset tooling.
28
+
29
+ ## When & why to use
30
+
31
+ - Publishing any `@scope/*` package (our libs: oblaka, lopata, opice, buzola, pletivo, @roj-ai/*) — secure, tokenless, provenance-stamped.
32
+
33
+ ## When NOT to use
34
+
35
+ - Private/unpublished packages, or apps (apps **deploy**, see `app-deploy`).
36
+
37
+ ## Gotchas
38
+
39
+ - **`id-token: write` is mandatory** — without it the OIDC exchange fails; there's no token fallback by design.
40
+ - **npm ≥ 11.5.1** for OIDC trusted publishing; pin Node/npm in the runner.
41
+ - **Trusted publishing must be configured on the npm package** (the repo/workflow is registered as a trusted publisher) before the first run.
42
+ - **Monorepo multi-package**: rewrite each `package.json` version from the tag **and resolve `workspace:*`** refs to the concrete version before publishing each package.
43
+
44
+ ## Relationships — why
45
+
46
+ - A CI pattern (no runtime tech deps). **recommends `bun`** (build/pack) and **`monorepo`** (multi-package publish coordination). The lib-side counterpart of `app-deploy`.
@@ -0,0 +1,47 @@
1
+ ---
2
+ id: monorepo
3
+ kind: pattern
4
+ title: Monorepo (Bun workspaces + flat catalog + project references)
5
+ slot: null
6
+ provides: [monorepo, workspaces, shared-packages]
7
+ requires: [bun]
8
+ recommends: [deptective]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "Bun workspaces documentation"
13
+ read: "Workspace manifests, dependency ranges, and filtered scripts."
14
+ - where: "roj — root package.json + packages/* (the same Bun-workspaces shape for @roj-ai/*)"
15
+ read: "A second real Bun-workspaces monorepo to cross-check conventions."
16
+ ---
17
+
18
+ # Monorepo
19
+
20
+ ## What it is
21
+
22
+ A **Bun workspaces** monorepo: `packages/*`, scoped `@app/<kebab>` package names,
23
+ cross-package deps via **`workspace:*`** (resolved to symlinks at install), a **single
24
+ flat `catalog`** in the root `package.json` as the one source of truth for transitive
25
+ versions, and **`tsc --build` project references** for type-checking. One linter +
26
+ one formatter across the tree (e.g. oxlint + dprint).
27
+
28
+ ## When & why to use
29
+
30
+ - A backend + multiple frontends (web/mobile/worker) that **share domain types + logic** (a `shared` package of Zod schemas, a `lib` of cross-cutting utilities).
31
+ - You want one lock file, cross-package tests, and no npm round-trip during dev.
32
+
33
+ ## When NOT to use
34
+
35
+ - Packages need **independent release schedules** → separate repos + npm.
36
+ - Fundamentally different build systems per package.
37
+
38
+ ## Gotchas
39
+
40
+ - **`workspace:*` = symlinks, not copies** — import from the package root (`import '@app/lib'`), never reach into `node_modules`.
41
+ - The **root `workspaces` list is the source of truth** — add a package dir _and_ run `bun install` or it isn't discovered.
42
+ - **One flat `catalog`**, no `catalogs.*` groups; bump versions in one place.
43
+ - **`tsc --build` needs `composite: true`** per package + a `references` list in the root tsconfig, or the build breaks.
44
+
45
+ ## Relationships — why
46
+
47
+ - **requires `bun`** — Bun workspaces + the catalog are the mechanism. **recommends `deptective`** — keep cross-package deps honest (missing/unused, project-reference drift).
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: named-seeds
3
+ kind: pattern
4
+ title: Named idempotent seeds (vs self-provisioning)
5
+ slot: seed-strategy
6
+ provides: [seed-strategy, foundation-manifest]
7
+ requires: [d1]
8
+ recommends: [opice]
9
+ conflicts: []
10
+ npm: []
11
+ status: stable
12
+ sources:
13
+ - where: "Pattern. Reference: opice skeleton metadata `seeds: [...]` + fabrika `induce`."
14
+ read: "How the union of `seeds` across skeletons induces a foundation 'seed infra' issue."
15
+ ---
16
+
17
+ # Named idempotent seeds
18
+
19
+ ## What it is
20
+
21
+ The seed strategy fabrika prefers: **one SQL file per named seed** (`seeds/<name>.sql`,
22
+ filename = opice `seeds: ['<name>']`), purely **idempotent** (`INSERT OR IGNORE` /
23
+ `ON CONFLICT DO NOTHING`, keyed on stable ids), applied before a test. The
24
+ alternative is **self-provisioning** (register through the UI with `Date.now()`
25
+ markers, `seeds: []`).
26
+
27
+ ## When & why to use
28
+
29
+ - You want "seeds = foundation manifest": fabrika induces a foundation data issue from the **union of `seeds`** across skeletons. Named seeds make that induction strong.
30
+ - Data that pre-exists for a flow (catalog, members, projects, mixed issues).
31
+
32
+ ## When NOT to use (fallback)
33
+
34
+ - Flows where a seed makes no sense (**signup**) → `seeds: []` + **self-provisioning** with `Date.now()` markers. This is the explicit fallback, not a competing slot.
35
+
36
+ ## Impacts & gotchas
37
+
38
+ - **Idempotent or it breaks the verify loop** — the app stays up between iterations and reseeding must tolerate repeats.
39
+ - Some seeds also stage non-D1 fixtures (R2 object, KV key) via a small `seeds/<name>.ts`.
40
+ - Apply: `lopata d1 execute DB --file seeds/<name>.sql` (local-first) or `wrangler d1 execute DB --local --file …`.
41
+
42
+ ## Relationships — why
43
+
44
+ - **requires `d1`** — seeds are SQL into the relational store.
45
+ - **recommends `opice`** — the filename↔metadata link (`seeds: ['<name>']`) is the contract that drives induction.
@@ -0,0 +1,45 @@
1
+ ---
2
+ id: observability
3
+ kind: pattern
4
+ title: Observability on Workers (logs + metrics via Analytics Engine + traces)
5
+ slot: null
6
+ provides: [observability, tracing, metrics]
7
+ requires: []
8
+ recommends: [analytics-engine, structured-logging, sentry]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "Cloudflare Analytics Engine docs"
13
+ read: "writeDataPoint, SQL queries, sampling behavior, and binding limits."
14
+ - where: "Cloudflare docs (Workers observability logs/traces, Analytics Engine) + oblaka's AE resource and `observability: { enabled: true }`"
15
+ read: "How logs/traces are enabled in wrangler/oblaka, the AE writeDataPoint limits, and tail workers."
16
+ ---
17
+
18
+ # Observability on Workers
19
+
20
+ ## What it is
21
+
22
+ The three pillars, the way they fit Cloudflare Workers:
23
+
24
+ - **Logs** — `structured-logging` to stdout, with `observability: { logs: { enabled: true } }` in wrangler/oblaka so CF ingests them (CF **samples** under load → keep a durable handler for complete audit).
25
+ - **Metrics** — **Analytics Engine** via `writeDataPoint`, hidden behind an **`AnalyticsSink` port** with a **typed event union**; the AE adapter in prod, a capturing sink in tests (see `count-store` for the same local/AE split).
26
+ - **Traces** — W3C `traceparent` + AsyncLocalStorage span context; `observability: { traces }` + Workers tail workers. **No heavy OpenTelemetry SDK** — ports/adapters + structured spans cover it.
27
+
28
+ ## When & why to use
29
+
30
+ - Any production Worker — you want correlation (requestId/traceId across hops), metrics that survive bursts (AE append, not D1 UPDATE), and a path to debug incidents.
31
+
32
+ ## When NOT to use
33
+
34
+ - A throwaway/local-only app — structured logs alone suffice; skip AE wiring.
35
+
36
+ ## Gotchas
37
+
38
+ - **AE cardinality limits**: ≤1 index (≤96 B), ≤20 doubles, ≤20 blobs (~16 KB/row) — **no free-form strings as blobs**; use stable enums + ids.
39
+ - **CF samples logs** under load — don't rely on stdout logs for a complete audit trail; add a durable (DO) handler if you need every line.
40
+ - **Propagate `traceparent`** on outbound requests — CF doesn't auto-propagate across service bindings.
41
+ - **Sample at the source** if event volume is high (AE has no built-in sampling in the sink).
42
+
43
+ ## Relationships — why
44
+
45
+ - **recommends `analytics-engine`** (metrics backend, behind `count-store`/an AnalyticsSink), **`structured-logging`** (the logs pillar), **`sentry`** (errors → traces). A pattern, composes over whatever stack is present.
@@ -0,0 +1,74 @@
1
+ ---
2
+ id: single-worker-spa
3
+ kind: pattern
4
+ title: Single-Worker SPA in production, split frontend/backend in development
5
+ slot: app-delivery-shape
6
+ provides: [spa-hosting, worker-api, local-dev-split, single-deployable]
7
+ requires: [cloudflare, vite, buzola]
8
+ recommends: [lopata, vozka]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "Cloudflare Workers Static Assets docs"
13
+ read: "API-first Worker routing, the ASSETS binding, SPA fallback, and run_worker_first behavior."
14
+ - where: "Cloudflare Workers Static Assets docs"
15
+ read: "The current assets binding, run_worker_first and not_found_handling semantics."
16
+ ---
17
+
18
+ # Single-Worker SPA, two-process local dev
19
+
20
+ ## What it is
21
+
22
+ A Buzola SPA and a Worker API are separate processes during development but one
23
+ deployable Worker in production:
24
+
25
+ - **development:** Vite serves the SPA with HMR (normally `:5173`), lopata serves
26
+ the Worker and local bindings (normally `:8787`), and Vite proxies only the API
27
+ namespaces to lopata;
28
+ - **production:** Vite builds static assets, the Worker's `ASSETS` binding serves
29
+ them, and the same Worker handles API routes before falling back to the SPA.
30
+
31
+ ## When & why to use
32
+
33
+ - Internal dashboards/admin apps where SSR has no product value.
34
+ - You want fast frontend HMR and local CF bindings without deploying two services.
35
+ - Client routes need deep-link fallback while API paths remain real 404s.
36
+
37
+ ## Integration contract
38
+
39
+ 1. Vite emits the SPA to the directory declared by the Worker's assets binding.
40
+ 2. `vite.config.ts` proxies explicit namespaces such as `/rpc`, `/api` and
41
+ `/internal` to the local lopata Worker.
42
+ 3. Static assets use `not_found_handling: 'single-page-application'`.
43
+ 4. Prefer selective `run_worker_first` patterns for `/rpc`, `/api/*` and
44
+ `/internal/*`, so non-API assets/navigation skip the Worker entirely.
45
+ 5. The Worker handles the matched API routes; an unhandled path _inside an API
46
+ namespace_ returns 404 and never falls through to `index.html`.
47
+ 6. If whole-page middleware truly requires `run_worker_first: true`, the Worker
48
+ must explicitly forward every non-API request with `env.ASSETS.fetch(request)`.
49
+
50
+ ## Load-bearing gotchas
51
+
52
+ - **Do not proxy `/` in development.** Vite owns page/navigation requests; only
53
+ API namespaces go to lopata.
54
+ - **Do not let unknown API paths render the SPA shell.** Keep an explicit API
55
+ prefix guard before the assets fallback.
56
+ - New scaffolds should prefer
57
+ the supported route-array form unless they need middleware on every page/asset
58
+ request. It avoids unnecessary billed Worker invocations.
59
+ - `ASSETS` may be absent locally. Type it as optional and return a real 404 after
60
+ API handling in the two-process dev flow.
61
+ - Browser requests need cookies on same-origin production calls. In development,
62
+ use Vite's proxy so the SPA still calls relative URLs and auth behavior does not
63
+ fork by environment.
64
+ - Buzola is client-only; there is no Worker SSR entry in this pattern.
65
+ - Verify selective routing against the installed Wrangler schema (route arrays
66
+ require a sufficiently recent Wrangler); never guess glob support.
67
+
68
+ ## Relationships — why
69
+
70
+ - **requires `buzola` + `vite`** — this is specifically a client-rendered Buzola
71
+ bundle served by Vite locally and built as Worker assets.
72
+ - **requires `cloudflare`** — production delivery uses a Worker assets binding.
73
+ - **recommends `lopata`** for the local Worker and **`vozka`** for the single
74
+ production deployable.
@@ -0,0 +1,49 @@
1
+ ---
2
+ id: structured-logging
3
+ kind: pattern
4
+ title: Structured logging (handlers + child context + span/trace)
5
+ slot: null
6
+ provides: [structured-logging, log-correlation, leveled-logging]
7
+ requires: []
8
+ recommends: [observability]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "OpenTelemetry semantic conventions and W3C Trace Context"
13
+ read: "Stable field names, trace propagation, span context, and structured errors."
14
+ - where: "Sentry structured logs documentation"
15
+ read: "Level filtering, contextual fields, and error forwarding."
16
+ ---
17
+
18
+ # Structured logging
19
+
20
+ ## What it is
21
+
22
+ A `Logger` built from **composable handlers** (`createLogger([handlers], baseAttrs)`),
23
+ with leveled, **structured** entries (`logger.info("event", { fields })`), `.child({
24
+ requestId, userId })` to attach correlation context at a boundary, and **span/trace
25
+ context** (AsyncLocalStorage + W3C `traceparent`) so logs correlate across RPC/DO
26
+ hops. Injected via the DI container — **never raw `console.log`** in app code.
27
+
28
+ ## When & why to use
29
+
30
+ - Always, in a Worker/backend — searchable, correlatable logs (by requestId/traceId) instead of stringly console output that CF samples and drops.
31
+
32
+ ## Anti-patterns (avoid these)
33
+
34
+ 1. **`console.log` spam** — unstructured, no context, dropped under CF log sampling. → `logger.info("feature.event", { id })`.
35
+ 2. **Logging secrets / PII / tokens** — leaks to Sentry, CF logs, DO logs (unredacted). → log ids + safe enums only; redact at the boundary.
36
+ 3. **Logging in hot loops** — cardinality blowup + ring-buffer thrash. → log once with a `count`, not per item.
37
+ 4. **Stringly-typed messages** (`"User " + id + " failed"`) — unsearchable, unaggregatable. → kebab event + structured attrs: `logger.warn("auth.login.failed", { userId })`.
38
+ 5. **No requestId propagation** — logs fragment across services/DOs. → `.child({ requestId })` at the RPC boundary + propagate `traceparent`.
39
+ 6. **Swallowing errors** (`catch { logger.warn("failed") }` without the error) — stack lost, Sentry blind. → `logger.error("op.failed", err, { ctx })`.
40
+
41
+ ## Best practices
42
+
43
+ - One logger built in the DI container; `.child()` per request with `requestId`/`userId`.
44
+ - Kebab `feature.scope.event` message names + structured attrs; levels mean what they say.
45
+ - For complete (un-sampled) audit logs, add a durable handler (e.g. a DO ring-buffer), not just stdout.
46
+
47
+ ## Relationships — why
48
+
49
+ - A pattern (no tech deps). **recommends `observability`** — structured logs are one of its three pillars; the Sentry handler ties into `sentry`.
@@ -0,0 +1,87 @@
1
+ ---
2
+ id: typed-worker-rpc
3
+ kind: pattern
4
+ title: Typed SPA-to-Worker RPC boundary
5
+ slot: client-worker-api
6
+ provides: [typed-rpc, client-worker-api, validated-input, centralized-auth]
7
+ requires: [cloudflare, buzola]
8
+ recommends: [propustka]
9
+ conflicts: []
10
+ status: stable
11
+ sources:
12
+ - where: "Cloudflare Workers RPC documentation"
13
+ read: "Typed method boundaries, input validation, response envelopes, and centralized authentication."
14
+ ---
15
+
16
+ # Typed SPA-to-Worker RPC
17
+
18
+ ## What it is
19
+
20
+ A small application-owned RPC boundary between a Buzola SPA and its Worker. The
21
+ Worker composes a method map; each method owns an input parser and a handler. A
22
+ type-only projection of that map gives the browser client fully inferred method
23
+ names, inputs and outputs without shipping Worker implementation code.
24
+
25
+ This is a pattern, not a framework dependency. It keeps the client-rendered stack
26
+ small while preserving end-to-end types and one authentication boundary.
27
+
28
+ ## Contract
29
+
30
+ ```ts
31
+ interface RpcMethod<Input, Output> {
32
+ input: { parse(value: unknown): Input }
33
+ handle(
34
+ input: Input,
35
+ auth: AuthContext,
36
+ request: Request,
37
+ ): Output | Promise<Output>
38
+ }
39
+
40
+ type InferRpcMethods<M> = {
41
+ [K in keyof M]: M[K] extends RpcMethod<infer I, infer O>
42
+ ? { input: I; output: Awaited<O> }
43
+ : never
44
+ }
45
+ ```
46
+
47
+ - The SPA posts `{ method, input }` to one relative endpoint such as `/rpc`.
48
+ - The Worker parses JSON, rejects an unknown method, validates input, invokes the
49
+ registered handler and returns either `{ result }` or a typed `{ error }`.
50
+ - The client imports the combined method-map type with **`import type`** and
51
+ exposes `callRpc(method, input)` for Buzola loaders plus a stable hook for
52
+ component mutations.
53
+ - The Worker authenticates once at the RPC boundary and passes `AuthContext` to
54
+ the selected method. Each method still performs its own `auth.can(...)` check.
55
+
56
+ ## Load-bearing gotchas
57
+
58
+ - **The client/server bridge must be type-only.** A value import from the Worker
59
+ method registry can pull Cloudflare-only modules and secrets into the browser
60
+ bundle.
61
+ - Validate both the envelope and every method input. A TypeScript type is not
62
+ runtime validation; reject malformed JSON/input with 400.
63
+ - Method names are an explicit allow-list. Never dispatch an arbitrary property
64
+ path or instantiate a handler from user input.
65
+ - Normalize known failures (validation 400, unauthorized 401, forbidden 403,
66
+ not found 404); log unexpected errors server-side and return a safe 500.
67
+ - With Propustka, preserve `loginUrl` in the 401 envelope and attach a returned
68
+ `setCookie` to the final RPC response. The SPA redirects a human miss to login
69
+ and guards against redirect loops.
70
+ - Use relative `/rpc` URLs. The Vite proxy owns the local backend origin; this
71
+ keeps cookie and request code identical in development and production.
72
+
73
+ ## Feature composition
74
+
75
+ Keep each feature's methods in a factory/class with constructor-injected ports
76
+ (D1 store, queue, provider, clock). Merge their method maps in one
77
+ `buildRpcMethods(env, ctx)` composition root and export only the inferred map
78
+ type. Unit-test method factories directly with an in-memory/fake AuthContext;
79
+ exercise the envelope/auth seam in a smaller set of integration tests.
80
+
81
+ ## Relationships — why
82
+
83
+ - **requires `cloudflare`** — the server side is the app Worker.
84
+ - **requires `buzola`** — the client surface is designed for loaders + SPA
85
+ mutations, not SSR actions.
86
+ - **recommends `propustka`** — central boundary authentication plus per-method
87
+ capability checks is the intended secure shape.
@@ -0,0 +1,74 @@
1
+ ---
2
+ id: vozka-app-deploy
3
+ kind: pattern
4
+ title: App deploy — Vozka-managed Cloudflare lifecycle
5
+ slot: null
6
+ provides: [ci-deploy, cf-deploy, infra-provisioning, schema-reconciliation, secret-sync]
7
+ requires: [vozka, propustka, oblaka, wrangler]
8
+ recommends: []
9
+ conflicts: [app-deploy]
10
+ status: wip
11
+ sources:
12
+ - where: "vozka — MIGRATION.md + packages/core/src/{plan,deploy}.ts"
13
+ read: "Registry fields, trigger routing, deploy locks, step order, state continuity and migration/cut-over rules."
14
+ - where: "vozka examples"
15
+ read: "A native-Propustka app shape with one resource graph plus its schema, build, and secrets pipeline."
16
+ ---
17
+
18
+ # Vozka-managed app deploy
19
+
20
+ ## What it is
21
+
22
+ The application repository declares its complete deploy surface in
23
+ `vozka.config.ts`; the account's Vozka control plane owns GitHub triggers/manual
24
+ runs, deploy credentials, per-app/per-env locking, logs and execution. The app no
25
+ longer maintains a workflow that manually sequences oblaka, wrangler, Propustka
26
+ and secrets.
27
+
28
+ ## Scaffold contract
29
+
30
+ - Commit `vozka.config.ts` with a stable app `id`, environment-neutral
31
+ `resources(ctx)`, optional Propustka `schema`, and `pipeline`.
32
+ - Export the resource builder for the local `oblaka.ts` shim so the graph does
33
+ not drift between lopata and production.
34
+ - Read the production hostname from `ctx.domain`.
35
+ - Bind `IAM: ServiceReference('propustka-worker')` only off-local; set a single
36
+ explicit local-dev flag for the app's deterministic auth context.
37
+ - Declare third-party runtime secrets in `pipeline.secrets`. Vozka platform
38
+ Cloudflare credentials and the Propustka provisioning key are not app secrets.
39
+ - Register repository, environment, domain, trigger ref and secret values in
40
+ the target account's Vozka. That external registration is an operator step,
41
+ not committed application config.
42
+
43
+ ## Deploy order (owned by Vozka)
44
+
45
+ 1. run `pipeline.build`,
46
+ 2. provision/materialize the oblaka resource graph into `wrangler.jsonc`,
47
+ 3. apply every declared D1 migration remotely,
48
+ 4. run `wrangler deploy`,
49
+ 5. reconcile the Propustka authz schema,
50
+ 6. sync declared Worker secrets.
51
+
52
+ The exact plan comes from the installed Vozka engine. Do not reproduce this
53
+ sequence in a parallel GitHub Actions workflow.
54
+
55
+ ## Migration/cut-over gotchas
56
+
57
+ - Preserve `<app-id>-state`; dry-run and verify that the first Vozka deploy sees
58
+ existing resources instead of creating replacements.
59
+ - The first schema reconcile should be a no-op. Diff committed actions/scopes/
60
+ roles against live Propustka before cut-over.
61
+ - A multi-account app is registered in multiple independent Vozka instances;
62
+ branch/ref routing decides which account acts on a push.
63
+ - During migration, only one deploy path may own a target. Delete the old
64
+ workflow after a real Vozka deploy is green, not before.
65
+ - Vozka is 0.x: verify current dry-run credential/network requirements instead
66
+ of assuming a dry-run is offline.
67
+
68
+ ## Relationships — why
69
+
70
+ - **requires `vozka`**, which orchestrates **`oblaka` + `wrangler`**.
71
+ - **requires `propustka`** because schema reconciliation and the ecosystem's
72
+ control-plane authentication are part of this lifecycle.
73
+ - **conflicts `app-deploy`** — that pattern is the direct per-repo workflow
74
+ alternative. One target must not deploy through both.
@@ -0,0 +1,47 @@
1
+ ---
2
+ id: write-split
3
+ kind: pattern
4
+ title: Three-store write split (AE counts / Queue→R2 payloads / D1 state)
5
+ slot: null
6
+ provides: [ingest-architecture]
7
+ requires: [analytics-engine, queue, r2, d1]
8
+ recommends: [count-store]
9
+ conflicts: []
10
+ npm: []
11
+ status: stable
12
+ sources:
13
+ - where: "Pattern. Reference: the cf-fat stack (ingest route + queue consumer)."
14
+ read: "How an error-tracker-style bursty write workload is split across primitives."
15
+ ---
16
+
17
+ # Three-store write split
18
+
19
+ ## What it is
20
+
21
+ An ingest architecture for **bursty write workloads** (error trackers, event
22
+ pipelines): split each write by shape —
23
+
24
+ - **counts → Analytics Engine** (append via `count-store`, no UPDATE),
25
+ - **full payloads → Queue → R2** (202 fast, consumer batches puts),
26
+ - **mutable state → D1** (only human-driven rows: issues, projects, config),
27
+ - **keys / dedup / session → KV**.
28
+
29
+ ## When & why to use
30
+
31
+ - Per-event `UPDATE count` on a D1 row would be hot-row contention an error storm kills.
32
+ - High-volume ingest where the request path must stay fast and writes must absorb bursts.
33
+
34
+ ## When NOT to use
35
+
36
+ - Low write volume / no spikes — a single `d1` store is simpler and correct.
37
+
38
+ ## Impacts & gotchas
39
+
40
+ - Ingest route returns **202** and enqueues; it does **NOT** write D1 synchronously per event.
41
+ - The `queue` consumer does R2 payload write + D1 upsert + `count-store.record`.
42
+ - Frozen as a tech-gate decision when chosen — it shapes the whole data flow.
43
+
44
+ ## Relationships — why
45
+
46
+ - **requires `analytics-engine` + `queue` + `r2` + `d1`** — it is precisely the composition of all four.
47
+ - **recommends `count-store`** — the counts arm goes through it so dev/test stay account-free.
@@ -0,0 +1,44 @@
1
+ ---
2
+ id: analytics-engine
3
+ kind: technology
4
+ title: Cloudflare Analytics Engine (counts / time-series)
5
+ slot: null
6
+ provides: [timeseries-counts, metrics]
7
+ requires: [cloudflare, count-store]
8
+ recommends: [d1]
9
+ conflicts: []
10
+ npm: []
11
+ status: stable
12
+ sources:
13
+ - where: "developers.cloudflare.com/analytics/analytics-engine (writeDataPoint, SQL API, _sample_interval)"
14
+ read: "writeDataPoint limits (≤20 blobs/doubles, 1 index), the SQL HTTP query endpoint, and SUM(_sample_interval) sampling."
15
+ ---
16
+
17
+ # Cloudflare Analytics Engine
18
+
19
+ ## What it is
20
+
21
+ High-cardinality, append-only time-series counts. You `env.AE.writeDataPoint(...)`
22
+ on the write path (no UPDATE, no hot row) and query aggregates over the **SQL HTTP
23
+ API** with `SUM(_sample_interval)`. The right home for "how many of X, grouped by
24
+ Y, over time" at error-storm volume.
25
+
26
+ ## When & why to use
27
+
28
+ - Event counts / trends / grouping where per-row UPDATE in D1 would be hot-row contention (e.g. error-tracker issue counts).
29
+ - Append-only metrics you read as aggregates, not individual rows.
30
+
31
+ ## When NOT to use
32
+
33
+ - Mutable state or exact read-after-write → `d1` / `durable-objects`.
34
+ - Anything you need locally without an account, **unless** abstracted (see below).
35
+
36
+ ## Impacts & gotchas — the load-bearing one
37
+
38
+ - **No local runtime.** `writeDataPoint` is a no-op locally and queries hit the account HTTP SQL API (needs a CF token). So **counts MUST go behind `count-store`** (local/in-memory/D1 impl for dev+tests, AE impl for prod) — otherwise build/opice tests need a CF account.
39
+ - `writeDataPoint`: ≤20 blobs + ≤20 doubles + **1 index**; queries use `SUM(_sample_interval)` to account for sampling.
40
+
41
+ ## Relationships — why
42
+
43
+ - **requires `cloudflare`** (binding) **and `count-store`** (the abstraction that keeps dev/test account-free).
44
+ - **recommends `d1`** — the local/test `count-store` impl typically aggregates from a D1 table.
@@ -0,0 +1,55 @@
1
+ ---
2
+ id: bindx
3
+ kind: technology
4
+ title: Bindx — typed React data binding for Contember
5
+ slot: data-binding
6
+ provides: [react-data-binding, form-state, persistence, schema-codegen, undo-redo]
7
+ requires: [contember, react]
8
+ recommends: []
9
+ conflicts: []
10
+ npm: ["@contember/bindx@^0.1.41", "@contember/bindx-react@^0.1.41", "@contember/bindx-generator@^0.1.41", "@contember/bindx-client@^0.1.41"]
11
+ status: wip
12
+ sources:
13
+ - where: "bindx/packages/example/ (article-view.tsx, article-editor.tsx, App.tsx, generated/index.ts)"
14
+ read: "Real usage: Entity/Field/HasOne/HasMany components, usePersist, and the generated schema the app imports."
15
+ - where: "bindx/packages/bindx-react/src/index.ts"
16
+ read: "The hook + component surface (useEntity, useEntityList, usePersist, useUndo; Entity, Field, HasOne, HasMany, If, Show; ContemberBindxProvider)."
17
+ - where: "bindx/packages/bindx-generator/src/generate.ts"
18
+ read: "How `bun run generate:schema` turns a Contember Model (+ optional ACL) into typed entities + a schema object."
19
+ ---
20
+
21
+ # Bindx
22
+
23
+ ## What it is
24
+
25
+ A **typed, schema-bound React data-binding layer** over a Contember GraphQL API.
26
+ You define the model in Contember, the generator emits TypeScript entity types + a
27
+ schema object, and bindx gives you live data access, form state, dirty tracking,
28
+ persistence, undo/redo, and field events — fully typed. Instead of
29
+ `useState` + manual GraphQL + a form lib, you get `useEntity` / `useEntityList` +
30
+ `<Entity>`/`<Field>`/`<HasMany>` and a single `usePersist()` that builds and posts
31
+ the Contember mutations.
32
+
33
+ ## When & why to use
34
+
35
+ - You have a **Contember** backend and are building a **React admin/CRUD/editor** that needs form state + binding.
36
+ - You want **strict typing**: a model change breaks TypeScript immediately.
37
+ - You want **persistence as one call** + free **undo/redo** + fine-grained field subscriptions.
38
+
39
+ ## When NOT to use
40
+
41
+ - **Not using Contember** — bindx assumes Contember's GraphQL/mutation shape (core is adapter-pluggable, but the generator + ecosystem are Contember-shaped).
42
+ - Lightweight client-only form state with no backend → `react-hook-form` + a store.
43
+
44
+ ## Impacts & gotchas
45
+
46
+ - Requires a **live Contember API** + a **codegen step** (`generate:schema`) you run before TypeScript sees the entity types; commit the generated files.
47
+ - **Selection is collected implicitly from JSX** — nesting unused relations over-fetches. Use `SelectionScope` / `debugSelection()`.
48
+ - **Role-aware generation (ACL)** produces per-role entity types (PublicArticle/EditorArticle/…), which adds type complexity but enforces security.
49
+ - **Undo is local-snapshot**, not a server transaction — a half-failed `persistAll()` undoes locally only; recover by refetching.
50
+
51
+ ## Relationships — why
52
+
53
+ - **requires `contember`** — it is a binding layer over a Contember GraphQL API; no Contember, no bindx.
54
+ - **requires `react`** — `@contember/bindx-react` ships React hooks/components (React 18+).
55
+ - **slot `data-binding`** — orthogonal to the router; it composes with the rest of the stack.