A store's data may live in its organization's own Firebase project (the
"space cloud", docs/space-cloud-ops.md); module code obeys these fences
from the start. They are code rules, not conventions.
| Concern | Where | Rule |
|---|---|---|
Install doc, state/, outbox/, logs/, the records collections (modules/{id}/{collection}/…) |
store/{sid}/modules/** → the store's cloud (any ≥ 3-segment store/{sid}/… path follows the store) |
read / written only through Repository.* with Ref.store.id(sid).modules… paths; an unreachable cloud is a typed 503 org_unreachable in a request and is caught per store in a sweeper |
Install token, module_key, session_key, inbound_key, wasm_hash, object, limits, egress |
the platform's secret store (admin-eur3, system/{sid}) → main |
the gateway trusts custody, never the install doc, for anything that decides what runs with what budget; a client editing store/{sid}/modules/{id} in their own console can change enabled, config, endpoint — all of which affect only their own module |
| Catalog + registry | modules/{key} (Ref.modules) → main by the resolver (bindingFor's modules case) |
publish is /system/** and the root is not reachable through /admin/* crud; a client project can never edit a registry record — which is why custody may copy hashes and limits from it. Until 2026-10 the registry lived at store/incore/modules/{key} — incore's own install path, so incore could not install a module; the one-off POST /system/module/registry/migrate moved both envs' records in 2026-10 and was deleted on 2026-10-27 (plans/console/module-registry-path.md), and install readers keep skipping a registry-shaped doc (installsOnly) until every env is migrated |
| Package objects (platform and organization) | modules/{platform|org/{org}}/{id}/{version}/… in the env's core bucket (mainBinding().bucket) |
Registry.* never writes to the store's (organization's) bucket; a store delete wipes store/{sid}/ in the org bucket and never touches modules/; the runner SA (later) reads modules/ in one core bucket per env |
| Module-owned elements + the runtime doc | store/{sid}/elements/{moduleId}-… and elements/{moduleId}_runtime → the store's cloud, beside the store's own elements |
written by the mount through Repository (module_elements.ts), never through crud; API writes pass guardElementWrite. The runtime doc is what the renderers read for the module's state (public mask ELEMENT_RENDER_MASK = html, css, js, module, disabled; origin / forked / runtime stay private) — an element-doc write, so it reaches the fleet as an ordinary elements hook, shim included |
| Metering | drop.stat rows (module_call, module_web) → BigQuery, central |
anything billed from stays in main — the meter of record is the stat stream, not a doc the client can edit; daily counters under state/_calls_* are advisory |
module_records event; setup/hooks invalidation |
cases in the store write dispatcher (triggers/store_dispatch.ts) |
fire in main and, through the shim, in every connected cloud with no shim release; the case receives both bodies and never re-reads. Every outbox/*, state/*, logs/* write in a bound cloud is one shim invocation the dispatcher answers with nothing — acceptable at v1 volumes |
| Transactions and batches | one project each | install / update / uninstall are sequences: registry read (main) → mint + custody (admin-eur3) → hook entries, collections, kits, install doc (the store's cloud) → the registry install index modules/{key}/installs/{sid} + installs_count (main, one transaction). /admin/commit is one project by construction |
| Sweepers (the outbox sweep, the schedule tick, auto-updates, the GC — by hand since 2026-10-24) | iterate stores inside withStore(sid) + a per-store try/catch |
no collection-group query across projects: update_available stamping and yank walk the registry's install index modules/{key}/installs/{sid} ({sid, version, source, installed_at}; the record keeps only installs_count), one store at a time |
| Private module ownership | the registry record {org}~{id} with owner: {kind: 'organization', id} and visibility: {organizations: [org]} |
the server takes the org from store/{sid}.organization, never from the request; every store of the org sees the package |
Hosting a connected module for an organization |
the org's own cloud, or any HTTPS host of theirs | self-service (firebase deploy with expressModuleRouter and the function URL in manifest.endpoint), or deploy: 'managed' — the platform deploys mod-{id}-{sid} into the store's cloud through the shim deploy queue with the org's custodied key (variant K only) and keeps its state in state/_deploy (§8.7) |
| Demo | custody is per core project and one Firebase project serves one environment | an install on a Demo store of an org without a Demo cloud lands in drop-core-demo main, exactly like its data |
| Indexes + TTL | firestore.indexes.json, deployed Demo first, picked up by each connected cloud on its next Re-check (/system/organization/verify) |
the logs.expires TTL is a field override; a module's collection.keys need no per-cloud index — _k.{name} is a single field Firestore indexes on its own in every project, so records queries work in a connected cloud the moment the install backfills them (data.indexes is deprecated); nothing at install touches a cloud's indexes |
/hook/module/* legs |
outside the request-store loop | each handler wraps its body in withStore(sid, …), so any stock / payments access follows the store instead of silently reading main |
| Deletion order | deleteStore |
Modules.uninstallAll(sid) runs after the sid resolves and before the subtree delete — it reads the install docs while they exist and deletes what a recursive delete never touches: tokens, custody, the registry installs entries (all main) |
Nothing here writes to the console's own database (drop-core-admin)
except the menu / overview docs the SPA already owns; who may manage a
module is the store's roster and the organization role, unchanged.