Example: Listen
Listen is the canonical Layer-2 TinyCloud app: a transcript workspace that syncs meeting transcripts (Fireflies, Granola, Google Meet) into the owner's own spaces and lets a TEE backend operate on that data through a single UCAN delegation — without the backend ever holding the owner's key or seeing secret plaintext. It is the worked example every other applications concept points to, because it exercises the whole stack: a manifest, the applications space, KV + SQL, one-signature capability composition, a delegate backend, and encrypted secrets.
Role
Listen sits in Layer 2 — a manifest app built by TinyCloud that enshrines a space. It is the reference instance for manifest-model, the applications space, and tee-backends: where those concepts describe a mechanism in the abstract, Listen is the concrete app_id, the concrete tables, and the concrete delegation that make them real. The read-only Feed explorer is an L3 consumer of exactly this data.
Mechanics
The manifest
listen/manifest.json is the app/data contract (manifest-model):
{
"manifest_version": 1,
"app_id": "xyz.tinycloud.listen",
"name": "Listen",
"defaults": true,
"secrets": {
"FIREFLIES_API_KEY": ["read"],
"GRANOLA_API_KEY": ["read"],
"ASSEMBLYAI_API_KEY": ["read"],
"DEEPGRAM_API_KEY": ["read"],
"GOOGLE_MEET_TOKENS": { "scope": "listen", "actions": ["read","write","delete"] }
},
"permissions": [
{ "service": "tinycloud.hooks", "path": "sql/conversations/conversation",
"actions": ["subscribe"], "skipPrefix": true }
]
}
app_id: xyz.tinycloud.listenis the stable namespace and the default path prefix for everything Listen writes.defaults: truerequests the built-in app-scoped tier — KV + SQL read/write under thexyz.tinycloud.listen/prefix — in addition to the one explicitpermissions[]entry.- No
spacefield, so app data defaults to theapplicationsspace (tinycloud:{did-suffix}:applications/…). No top-leveldid, so the manifest itself is not a delegation target; the app session key is the actor and the TEE backend is added as a delegate at compose time. secrets{}declares vault entries; the SDK composer maps each to atinycloud.kv/getgrant onvault/secrets/<NAME>in the distinctsecretsspace.- The lone
permissions[]entry is atinycloud.hooks/subscribecap (withskipPrefix: true, an absolute service-namespace path) for live conversation-row write events.
Where the data lives
All app data is in the owner's applications space, keyed under xyz.tinycloud.listen:
| Data | Service / path | Shape |
|---|---|---|
| Conversations | SQL db xyz.tinycloud.listen/conversations | conversation, participant tables |
| Transcripts | KV xyz.tinycloud.listen/transcript/<conversationId> | TranscriptSentence[] JSON blob |
The SQL schema is PRIMARY KEY-only — TinyCloud's SQLite authorizer forbids CREATE INDEX, UNIQUE, and REFERENCES, so dedup is done in app code (backend/src/schema.ts):
CREATE TABLE IF NOT EXISTS conversation (
id TEXT PRIMARY KEY, title TEXT, source TEXT NOT NULL, source_id TEXT,
source_url TEXT, started_at TEXT, ended_at TEXT, duration_secs REAL,
summary TEXT, metadata TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS participant (
id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, name TEXT NOT NULL,
email TEXT, speaker_label TEXT
);
persistConversation() writes the conversation row, the participant rows, and then the transcript KV blob (backend/src/services/persist-conversation.ts) at key xyz.tinycloud.listen/transcript/<conversationId>.
The backend is a delegate, never a key-holder
Listen's backend is the canonical TEE backend. It advertises a DID plus the permissions it wants over /api/server-info (backendDelegationPermissions() in backend/src/manifest.ts): KV get/put/del/list/metadata under the app prefix, SQL read/write on conversations, a tinycloud.kv/get on each vault/secrets/<NAME> in the secrets space, and tinycloud.encryption/decrypt on the owner's default encryption network urn:tinycloud:encryption:<ownerDid>:default.
The frontend folds those into the manifest as a backend delegate (same app_id, with a did, defaults: false) and composes app + backend into one capability request the user signs once. After sign-in the SDK materializes the backend's UCAN from the existing session — tcw.materializeDelegation(backendDID, capabilityRequest) — with no second wallet prompt (packages/client/src/delegation.ts), then POSTs the serialized PortableDelegation to /api/delegations. The backend activates it against the node via node.useDelegation(...) and reads secrets only by asking the node to decrypt under the delegated network grant (backend/src/delegation-activation.ts) — so it never sees secret plaintext and never holds the owner's root key. It also self-checks that its requested caps are a subset of what the manifest grants (isCapabilitySubset), which is the subset rule in action.
How it was built
Listen is the frontend + backend shape, and it follows the exact path a new app follows with Getting Started and tinyboilerplate:
- Manifest — author
manifest.json(manifest-model) declaringapp_id: xyz.tinycloud.listen, thedefaults: truetier, thesecrets{}block, and the onetinycloud.hooks/subscribepermission. - Composition — the frontend fetches the backend's advertised policy from
/api/server-info, turns it into a backend delegate manifest (sameapp_id, adid,defaults: false), and composes[appManifest, backendManifest]into one capability request. - Sign-in — the user signs that composed SIWE/ReCap once (sign-in-flow); the session key now holds the whole union and the backend
didis pre-authorized. - Delegation — the frontend materializes the backend's PortableDelegation from the session key with no second prompt (
materializeDelegation) and POSTs it to/api/delegations; the backend activates it withnode.useDelegation(...). - Backend persistence — using only the delegated capability, the TEE backend writes the
conversation/participantrows to SQL and the transcript blob to KV in the owner'sapplicationsspace, decryptingsecretsthrough the node.
The difference between Listen and the blank starter is only the manifest, the data model, and the backend routes — the compose → sign-once → materialize → operate spine is identical.
Shape
The on-disk contract is manifest.json above. The runtime identity is app_id = xyz.tinycloud.listen; the owner DID is did:pkh:eip155:1:<address>; the addressable data is tinycloud:pkh:eip155:1:<addr>:applications/{sql|kv}/xyz.tinycloud.listen/… (see uri-addressing-grammar).
Relationships
Built from a manifest; enshrines the applications space; stores via SQL + KV; authorized through composed capabilities and one delegation; operated by a TEE backend; reads secrets from the secrets space decrypted on an encryption network; registered in the install registry in the account space; read downstream by Feed. The full source trace is the apps map.
Example
A user signs in to Listen with their wallet (OpenKey) once. That single SIWE/ReCap signature authorizes the app session key and materializes a UCAN for the TEE backend. Fireflies syncs a meeting: the backend writes a conversation row + participant rows to SQL xyz.tinycloud.listen/conversations and the transcript JSON to KV xyz.tinycloud.listen/transcript/<id>, all in the owner's applications space, after decrypting FIREFLIES_API_KEY from secrets/vault/secrets/FIREFLIES_API_KEY through the node. Later, Feed self-grants read caps as the owner and queries the same rows over the tc CLI — same data, different identity, all under the owner's capabilities.
Status & drift
Shipped. Two source-level discrepancies are documented in the apps map (not resolved here): the listen-importer writes without --space, so importer-published conversations may land in default rather than applications; and two transcript KV path conventions (transcript/<id> vs importer/transcripts) coexist. Treat the backend-written paths above (persist-conversation.ts) as canonical for Listen-app data.
Sources
listen:manifest.json(the contract above),SPEC-manifest-and-capability-chain.md(manifest semantics,applicationsdefault, compose→materialize),backend/src/schema.ts(SQL schema +conversationsdb routing),backend/src/services/persist-conversation.ts(transcript KV path + write pass),backend/src/manifest.ts(backendDelegationPermissions,isCapabilitySubset),backend/src/delegation-activation.ts(node.useDelegation, secrets-via-node decrypt),packages/client/src/delegation.ts(materializeDelegation,prompted)- Map:
maps/apps-feed-listen.md