Protocol / Applications / Example: Listen
shippedLayer 2 · App

Example: Listen

Listen is the canonical Layer-2 TinyCloud app — app_id xyz.tinycloud.listen, defaults true — storing SQL conversations and KV transcript blobs in the applications space, operated by a TEE backend through one materialized UCAN delegation.

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.listen is the stable namespace and the default path prefix for everything Listen writes.
  • defaults: true requests the built-in app-scoped tier — KV + SQL read/write under the xyz.tinycloud.listen/ prefix — in addition to the one explicit permissions[] entry.
  • No space field, so app data defaults to the applications space (tinycloud:{did-suffix}:applications/…). No top-level did, 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 a tinycloud.kv/get grant on vault/secrets/<NAME> in the distinct secrets space.
  • The lone permissions[] entry is a tinycloud.hooks/subscribe cap (with skipPrefix: 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:

DataService / pathShape
ConversationsSQL db xyz.tinycloud.listen/conversationsconversation, participant tables
TranscriptsKV 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.

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, applications default, compose→materialize), backend/src/schema.ts (SQL schema + conversations db 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