Concepts
Immutable App/Release catalog with verified public website delivery
Native SQLite test adapter
Section titled “Native SQLite test adapter”Use the public testing entry with a caller-owned Node SQLite database:
import { DatabaseSync } from "node:sqlite";import { APP_RELEASE_D1_FRESH_SCHEMA } from "@fungi.computer/puffball/cloudflare";import { sqliteD1 } from "@fungi.computer/puffball/testing";
const sqlite = new DatabaseSync(":memory:");sqlite.exec("PRAGMA foreign_keys = ON");sqlite.exec(APP_RELEASE_D1_FRESH_SCHEMA);const database = sqliteD1(sqlite);const apps = await database .prepare("SELECT app_id FROM app_release_apps") .all();console.log(apps.results); // []sqlite.close();The adapter uses the public Watchdog Node SQLite owner. It supplies prepared statements and atomic batches for tests; it is not a Cloudflare emulator or isolation boundary. The caller owns schema setup and database lifetime.
Core concepts
Section titled “Core concepts”Records
Section titled “Records”| Record | Owns |
|---|---|
| App | Stable global identity registered in the catalog. |
| Release | Immutable App artifact reference, digest and source/build provenance. |
| Public Surface | An App-owned stable route with an explicit selected Release. |
| Transition receipt | The recorded result of an operation-scoped selection or revocation. |
IDs describe identity. They do not grant permission to change that identity.
Each ID has a fixed grammar, such as app_<a-z0-9>, rel_<a-z0-9>,
ps_<a-z0-9>, artifact:<name>, idem_<key>, git:<40 hex>,
sha256:<64 hex> and a single-segment route like /hello. Parse untrusted
strings with the root parse* functions. A rejected value throws
AppReleaseInputError naming the parser.
Entrypoints
Section titled “Entrypoints”The root entrypoint exports createAppReleaseCatalog, the parse* boundary
parsers, the record types, the error classes and the App declaration vocabulary.
The catalog is a Promise facade over an Effect core. The ./effect entrypoint
exposes that core (createAppReleaseCore), the decode* functions and the
in-memory store. Both use the same grammar and decisions.
App roots
Section titled “App roots”A Release artifact is an App root with media type
application/vnd.fungi.app+json and format: "fungi-app-v1". It binds a visual
part (an HTML page or an asset manifest), an optional durable backend artifact,
or both, under one digest-covered description. An App can be visual, headless or
both, but never empty. ./app builds and parses App roots, ./backend parses
backend descriptors, and ./assets parses asset manifests.
Declared capabilities, handlers, roles and commands describe what an App may ask its host to expose. They do not approve those operations.
App storage declarations
Section titled “App storage declarations”An App can include bounded keys for its own data in fungi.app.storage:
[{ "key": "desktop", "scopes": ["team", "member"], "maxBytes": 16384 }]Each digest-covered declaration allows at most 16 distinct keys. A key matches
[a-z][a-z0-9.-]{0,63}; its non-empty scopes are a unique subset of team and
member, and maxBytes is from 1 through 65,536. This requests storage for the
App’s own installation data. It grants no access to another App, installation or
Team; the Hub owns scoped admission and write authority.
Artifact readers
Section titled “Artifact readers”The artifact reader resolves a stored opaque reference and verifies it. Verification checks the expected digest, content type, configured size bounds and an owned copy of the bytes. A reader never uses a caller-supplied digest to find different bytes. Asset manifests also bind object versions, digests, lengths and a mount base. Your host keeps admission and document isolation.
Create, verify and select
Section titled “Create, verify and select”Create the App before its Releases and Surfaces. A Release or Surface for a
missing App throws AppReleaseOwnerAppAbsentError or
AppReleaseSurfaceOwnerAppAbsentError. Replaying the same immutable record
returns it. Reusing an identity with changed material fails, and for a Release
that is AppReleaseImmutableConflictError.
Creating a Release does not promote it. Verify its artifact, then request an
explicit promotion or rollback to an eligible Release that belongs to the
App. Unverified, revoked and cross-App selections fail. Retrying a transition
with the same idempotency key and input returns its receipt. Changed input under
that key throws AppReleaseTransitionIdempotencyConflictError and leaves the
selection alone.
A revocation stops eligible delivery and selection. It does not edit immutable
artifact bytes or decide how a Team restores an installation. Your host’s
backend state lifecycle and schema migrations are separate from Release facts.
Serving selected bytes
Section titled “Serving selected bytes”resolvePublicWebsite(route) takes a canonical stable route, reads the current
selection, verifies the artifact and rechecks authority before returning bytes.
A private cache never skips that recheck. Request bodies, cookies and
caller-supplied Release IDs cannot select unpublished content.
Cloudflare adapters
Section titled “Cloudflare adapters”./cloudflare provides createD1AppReleaseStore for catalog state,
createR2ArtifactReader for artifact reads, and APP_RELEASE_D1_FRESH_SCHEMA
for an empty database. ./cloudflare-assets reads and verifies asset builds
from R2. ./publisher holds the schemas for registering an App’s source
repository with a publishing Team. The adapters do not provision resources or
supply Team authorization.