Context
The mieweb target (@mieweb/cloud-os) is meant to interoperate with the orchestration in mieweb/opensource-server — LXC containers on Proxmox, fronted by auto-generated nginx, dnsmasq DNS, LDAP/oauth2-proxy auth, ACME TLS, reconciled by a site agent, and driven by the Manager web app (Node/Express/Sequelize + Postgres).
Today there is no coupling between the two repos. cloud-os is a runtime adapter set (D1→libSQL, R2→S3/MinIO, KV/Queues→Valkey, Vectorize→libSQL vectors, DO→in-proc); opensource-server is a provisioning/orchestration control plane. This issue captures the design discussion for how to bridge them.
How Cloudflare frames the problem (the model we're replicating)
Cloudflare splits into two planes:
- Control plane —
wrangler deploy reads wrangler.jsonc as a declarative resource manifest, calls the account API to ensure resources exist, and injects scoped binding handles into the Worker's env. State lives in independently-provisioned, separately-scaled managed services.
- Data plane — at request time
env.DB / env.BUCKET are live handles that RPC to those services. Compute is stateless/disposable.
@mieweb/cloud faithfully replicated the data plane (binding shapes) and delegates the control plane to wrangler on the cloudflare target. On mieweb there is no control plane — mieweb.jsonc + docker-compose is a hand-rolled substitute. The integration work is making opensource-server play the control-plane role.
Note: env on Cloudflare is not process.env — it's an object of live handles + string vars passed as the 2nd arg to fetch. Any OS env vars we use are host-bootstrap config, never the worker's env.
Binding identity vs. location (the ID format)
Model wrangler's beta automatic provisioning: IDs in wrangler.jsonc short-circuit provisioning; absent IDs get created and written back. We adopt Option A — the committed wrangler.jsonc is the only registry (documentation instructs committing it after deploy to persist IDs).
For IDs to be self-describing without the Manager persisting any driver-specific data, use a URI form in wrangler's native field:
mieweb+<driver> — marks it as ours (not a CF UUID) and is the registry-driver key.
- path/query — the resource identity + tuning (
?dim=768). Committed to git. Never contains endpoint or credentials.
Resolution = identity (from committed ID) + location/secrets (injected at boot) → the cfg object the existing driver registry already consumes. Boot-time: parseId(id) → {driver, name, opts}; merge with injected driver endpoint/creds; call getDriver(driver)(cfg) — unchanged below the seam (~30 lines in createCloudEnv/config loader). Fail fast in settleEnv if a driver's endpoint is missing.
The isolation requirement that reshapes everything
Many users share a site; one app's credentials must not reach another app's data. This rules out a site-wide shared-credential blob and pushes toward either per-app scoped credentials on shared stores (Manager must mint/custody admin creds) or per-app dedicated services (isolation becomes structural, no shared admin key exists).
Proposed architecture: single converged container per app
- Read
name from wrangler.jsonc (must be a valid DNS label; CLI rejects non-conforming names rather than silently slugifying).
- Create a container via the Manager API with hostname =
name — this enforces uniqueness per site (not truly global).
- Container is created from a converged template image (e.g.
ghcr.io/mieweb/cloud:latest) running MinIO + libsqld + Valkey (+ optional Ollama) locally under a supervisor (systemd units fit the LXC base). The CLI sets service credentials via environmentVars.
- The app is deployed into the same container — reaching co-located services over localhost (
127.0.0.1:9000/8080/6379); only the app itself is exposed publicly via the Manager's services (nginx + TLS). ⚠️ Open question — delivery mechanism:
- (A) per-app image
FROM the converged base → build + registry push + update-container(template=<digest>). Keeps the Manager purely image-based/dumb. Recommended target.
- (B) converged base fetches app at boot (env-pointed source). POC shortcut; slower/racier starts, needs somewhere to stage the bundle.
- (C) agent/volume sync — requires Manager/agent changes; avoid.
mieweb deploy is idempotent via hostname: list-containers?hostname=<name> → update, else create. Same name → same container; change name → new container.
Consequences of this model
- Isolation is structural (per-container) — no shared stores, no Manager credential-minting, no bleed.
- Manager stays dumb — reuses
create/update/list-container, services, environmentVars. Likely one small gap: a persistent volume for the container's now-critical local state (MinIO/libsqld/Valkey data) + a backup story.
MIEWEB_DRIVERS collapses — endpoints are constant localhost; only per-container secrets vary (or are generated in-container and never leave).
- Every distributed-coordination problem disappears — DO placement, queue multi-consumer, KV list cursors are non-issues because everything is single-node by design.
- Trade-off: no horizontal scale, no HA, heavy per-app baseline. These are scale-out concerns that don't bite at POC scale.
Decision: single-container-per-app (A) vs. shared services + control plane (B)
| Dimension |
A: Single container per app |
B: Shared services + control plane |
| Tenant isolation |
✅ Structural — no shared store or network path |
⚠️ Credential-scoped; only as strong as weakest store's ACLs (Valkey leaky) |
| Manager code changes |
✅ ~None (maybe one volume knob) |
❌ Significant — provisioning + credential-minting + admin-cred custody |
| Credential bleed risk |
✅ None — creds only open co-located services |
❌ Shared admin key is a real bleed vector |
MIEWEB_DRIVERS complexity |
✅ Collapses (localhost endpoints; per-container secrets) |
⚠️ Per-app scoped creds + endpoints minted per deploy |
| DO placement / queue consumers / KV list |
✅ Evaporate — single-node by design |
❌ All persist; must be solved |
| Horizontal scaling |
❌ Impossible (2 replicas = 2 MinIOs) |
✅ Native, Cloudflare-faithful |
| Fault tolerance / HA |
❌ Container = SPOF for compute and data |
✅ Worker crash cheap; services HA independently |
| Resource efficiency |
❌ Heavy per-app baseline even for tiny apps |
✅ Shared services amortize; thin workers, dense |
| Deploy mechanics |
⚠️ Per-app image build + push |
⚠️ Worker image, but no co-located stack to build |
| Backup / DR |
✅ Simple unit (snapshot one container) / ❌ many targets, state in-container |
⚠️ Centralized stores (fewer targets) / entangled per-app data |
| Cloudflare fidelity (semantic) |
❌ Low — abandons stateless/shared-state split |
✅ High — mirrors CF control-plane model |
| Operational / attack surface |
✅ Small — only app exposed; data on localhost |
❌ Larger — shared services network-reachable |
| Time-to-first-working |
✅ Fast — buildable image + existing API |
❌ Slow — blocked on Manager control-plane features |
| Cost of the hard problems |
Pushed into image build (tractable) |
Pushed into the Manager (distributed-systems + authz) |
| Failure blast radius |
✅ One app |
❌ Wider — shared-store outage hits every app |
| Fit for opensource-server |
✅ High — platform is a container provisioner |
⚠️ Medium — platform must grow a managed-services role |
The trade in one line
- A trades horizontal scale + efficiency for the disappearance of every hard problem (isolation, credential custody, DO placement, Manager changes). Cost lands in the image build and "one heavy container per app, no HA."
- B trades simplicity for scale + efficiency; it's Cloudflare-faithful and dense, but forces the Manager to become a real control plane and leaves all coordination problems on the table.
Recommendation
- Ship A now for the POC: only option runnable without turning the Manager into a control plane; makes isolation structural. Weaknesses (no scale/HA, heavy per-app) are later, scale-out concerns.
- Treat B as the eventual model when apps get numerous (per-app full stacks wasteful) or an app needs true HA/scale — at which point growing the Manager's control plane is justified.
- Hybrid path: the URI/
MIEWEB_DRIVERS design already supports per-driver, per-app endpoint overrides — a binding can point at localhost (A) or an external shared service (B) without a rewrite. Ship A, adopt B selectively later.
Open questions / follow-ups
Context
The
miewebtarget (@mieweb/cloud-os) is meant to interoperate with the orchestration inmieweb/opensource-server— LXC containers on Proxmox, fronted by auto-generated nginx, dnsmasq DNS, LDAP/oauth2-proxy auth, ACME TLS, reconciled by a site agent, and driven by the Manager web app (Node/Express/Sequelize + Postgres).Today there is no coupling between the two repos.
cloud-osis a runtime adapter set (D1→libSQL, R2→S3/MinIO, KV/Queues→Valkey, Vectorize→libSQL vectors, DO→in-proc); opensource-server is a provisioning/orchestration control plane. This issue captures the design discussion for how to bridge them.How Cloudflare frames the problem (the model we're replicating)
Cloudflare splits into two planes:
wrangler deployreadswrangler.jsoncas a declarative resource manifest, calls the account API to ensure resources exist, and injects scoped binding handles into the Worker'senv. State lives in independently-provisioned, separately-scaled managed services.env.DB/env.BUCKETare live handles that RPC to those services. Compute is stateless/disposable.@mieweb/cloudfaithfully replicated the data plane (binding shapes) and delegates the control plane to wrangler on thecloudflaretarget. Onmiewebthere is no control plane —mieweb.jsonc+ docker-compose is a hand-rolled substitute. The integration work is making opensource-server play the control-plane role.Note:
envon Cloudflare is notprocess.env— it's an object of live handles + string vars passed as the 2nd arg tofetch. Any OS env vars we use are host-bootstrap config, never the worker'senv.Binding identity vs. location (the ID format)
Model wrangler's beta automatic provisioning: IDs in
wrangler.jsoncshort-circuit provisioning; absent IDs get created and written back. We adopt Option A — the committedwrangler.jsoncis the only registry (documentation instructs committing it after deploy to persist IDs).For IDs to be self-describing without the Manager persisting any driver-specific data, use a URI form in wrangler's native field:
{ "d1_databases": [{ "binding": "DB", "database_id": "mieweb+libsql:bluehive-hum" }] } { "r2_buckets": [{ "binding": "RECORDINGS", "bucket_name": "mieweb+s3:bluehive-recordings" }] } { "kv_namespaces": [{ "binding": "SESSIONS", "id": "mieweb+valkey:bluehive/sessions" }] } { "vectorize": [{ "binding": "SEARCH_INDEX","id": "mieweb+libsql-vec:search_index?dim=768" }] }mieweb+<driver>— marks it as ours (not a CF UUID) and is the registry-driver key.?dim=768). Committed to git. Never contains endpoint or credentials.Resolution = identity (from committed ID) + location/secrets (injected at boot) → the
cfgobject the existing driver registry already consumes. Boot-time:parseId(id) → {driver, name, opts}; merge with injected driver endpoint/creds; callgetDriver(driver)(cfg)— unchanged below the seam (~30 lines increateCloudEnv/config loader). Fail fast insettleEnvif a driver's endpoint is missing.The isolation requirement that reshapes everything
Many users share a site; one app's credentials must not reach another app's data. This rules out a site-wide shared-credential blob and pushes toward either per-app scoped credentials on shared stores (Manager must mint/custody admin creds) or per-app dedicated services (isolation becomes structural, no shared admin key exists).
Proposed architecture: single converged container per app
namefromwrangler.jsonc(must be a valid DNS label; CLI rejects non-conforming names rather than silently slugifying).name— this enforces uniqueness per site (not truly global).ghcr.io/mieweb/cloud:latest) running MinIO + libsqld + Valkey (+ optional Ollama) locally under a supervisor (systemd units fit the LXC base). The CLI sets service credentials viaenvironmentVars.127.0.0.1:9000/8080/6379); only the app itself is exposed publicly via the Manager'sservices(nginx + TLS).FROMthe converged base → build + registry push +update-container(template=<digest>). Keeps the Manager purely image-based/dumb. Recommended target.mieweb deployis idempotent via hostname:list-containers?hostname=<name>→ update, else create. Samename→ same container; changename→ new container.Consequences of this model
create/update/list-container,services,environmentVars. Likely one small gap: a persistent volume for the container's now-critical local state (MinIO/libsqld/Valkey data) + a backup story.MIEWEB_DRIVERScollapses — endpoints are constant localhost; only per-container secrets vary (or are generated in-container and never leave).Decision: single-container-per-app (A) vs. shared services + control plane (B)
MIEWEB_DRIVERScomplexityThe trade in one line
Recommendation
MIEWEB_DRIVERSdesign already supports per-driver, per-app endpoint overrides — a binding can point at localhost (A) or an external shared service (B) without a rewrite. Ship A, adopt B selectively later.Open questions / follow-ups
FROMconverged base; define themieweb deploybuild/push/update-containerflow.create-containerAPI support volume specs, or is this the one real Manager gap?namenormalization/validation rules (DNS-label constraints vs. wrangler name rules); per-site vs. global uniqueness scoping.parseId/formatId/ensureExists/createcontract + thecreateCloudEnv/config-loader seam.X-User,X-Groups).