Use this bundle to read durable structured app data. entities is the core API; useLiveEntityQuery maintains a React subscription.
The entities.query request is type-scoped and its grant is dynamic: the host checks the caller, operation, type, and visible records. Inspect host.capabilities.list() and complete the host-reviewed request/grant flow for the exact type. A connected SDK or a TypeScript generic grants nothing. This example assumes an existing registered notes.note type with a string title field; substitute your actual registered type.
Merge this request fragment into the existing descriptor and complete host access review. A live subscription additionally needs watch; fetching an individual record needs read.
{ "requests": { "entities": { "notes.note": ["query"] } } }
Declare types under entityTypes[]. A valid type ID uses dotted lowercase
kebab-case segments:
^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$
Examples: crm.contact, media.generated-asset. Underscores, camelCase, and a
single undotted segment are invalid. "*" is a request selector only; it is not
an entity type declaration.
Each declaration needs an integer schemaVersion >= 1, object schema, indexes
(empty is valid), and display metadata with a label field. Evolution is
additive: bump the schema version; do not remove or incompatibly change prior
fields/indexes.
inferenceEgress is optional type metadata: it defaults to "allow"; set it
to "local-only" only for prompt-sensitive records. The host, not project
code, chooses the inference route and applies the most restrictive type in a
prompt context. This metadata grants no model, egress, or caller authority.
Core calls are listTypes, create, get, query, update, delete,
watch, and subscribe. subscribe is the live UI path; watch is the
long-poll fallback.
create, update, and delete accept an optional caller-stable
idempotencyKey; keyed create also requires a caller-stable id. Reuse the
same key only for the identical logical request: the host replays the original
authorized result without another record, event, outbox operation, or delete
confirmation. A changed request returns entity-idempotency-conflict. Omit
the key to retain ordinary mutation semantics. Keyed creates with initial
grants are refused while protected grant authority is active because that
cross-store grant commit is not retry-safe.
{ kind: 'assistant' } is the host-reserved projectless assistant principal.
It may appear in entity type/record/grant/event metadata returned by the SDK,
and an exact host-written entity grant may name it as the subject. Project code
never supplies a caller principal to an entity operation and cannot become the
assistant by constructing that object. The host-only assistant.task built-in
is presentation/recovery state for projectless work; projects must not create,
update, or treat those records as execution authority.
{ kind: 'community-member', projectId, membershipId } is host/server-stamped
community audit provenance. It can appear only in Entity metadata returned by
the SDK (record source and audit fields, events, and host-written grants); no
entity request accepts it as a caller, authority, or effective-principal
selector. It does not inherit project or agent authority, and it does not alter
site-user semantics.
Declare only requests.community: ["member"] to make the narrow Community
participation surface reachable. community.member.current() is grantless and
returns one explicit posture: project owner, active member, signed out, or not
a member. Member actions use the closed member.claim, attachment.issue, message.create,
message.edit, message.delete, reaction.set, search,
issue.create, issue.affect, and moderation.apply leaves. They require
that one declaration but no project-authored standing grant. Callers supply
only bounded content, host-issued opaque references, desired reaction state,
and idempotency keys. Main resolves the project, namespace, verified subject,
membership/profile/role, visibility, target authority, issue number,
timestamps, and every author/reporter/moderator audit field.
community.attachment.issue is the only Community byte ingress. It accepts a
bounded byte array, safe media type, display name, and the same idempotency key
as the future message; it returns only { receiptId, name, mediaType, bytes }.
The host binds that receipt to the current project, release/custody generation,
membership version and role, content digest, expiry, and deterministic message.
One receipt can be claimed only by that exact message intent.
For bytes selected directly by a user, render one ordinary
<input type="file">, read the selected File with arrayBuffer(), and pass a
bounded Uint8Array to community.attachment.issue. This policy is identical
for the desktop iframe and hosted browser shell. Do not read File.path, use
webUtils.getPathForFile, add a preload path bridge, ask for a directory or
File System Access handle, or assume ambient filesystem authority. files.pick
is the Files-library object powerbox; it is not the direct-byte picker and must
not be used as a hosted fallback.
Site Users stay distinct from Community Members, and a desktop project owner
stays distinct from either. A reviewed hosted release carries the same closed
Community intent methods over the existing data envelope and exposes reads
through community.projection.read plus
community.projection.subscribe. The hosted resolver derives the release,
project, Community namespace, Site User subject, custody generation, profile,
role, and provenance; a browser never supplies those selectors or receives a
generic Entity record. Its member posture projection is only signed-out,
project-owner, authenticated-not-member, or member with an
active/suspended/revoked posture and bounded profile display data
where appropriate. Message/channel/profile/search references are opaque and
authority-bound. The projection never returns account, Entity, binding,
generation, grant, D1, R2, or filesystem identifiers. The host keeps
recoverable journals and exact replay receipts around custody writes and does
not treat Site User authentication as membership. Private evidence and legacy
claimant authorization remain separate carriers.
For a Web-published public form, use that same entities.create(type, data)
call. ISPO may host it only when Web review derives one exact create-only type
from protected project authority and binds its registered schema into the
release. The browser runtime infers the anonymous visitor session; never pass a
principal, user/account id, tenant, realm, database, source, or grants. Hosted
anonymous forms reject non-empty create options and every other entity method.
This is a capability of one project release, not a web/form project type.
Entity reads are grant-filtered. Seeing only owned types is indistinguishable from an otherwise empty store. If a browser declared wildcard read but lacks the grant, render a needs-access state and call:
await entities.requestAccess('*', ['read', 'query', 'watch'])
Its result carries decision: 'allow' | 'deny' | 'pending' | 'expired' | 'blocked' | 'error', with an optional reason: 'apply-failed'. Branch on all
six; only deny means a human refused. error + apply-failed means the user
ALLOWED and the host failed to write the grant — report the host failure and ask
again, never render it as a rejection or send the user to Access. expired is a
lapsed undecided ask, blocked is a trust/declaration veto where no Allow was
ever offered, and pending means your caller deadline fired while the durable
ask is still open. Re-ask for pending, expired, and error if the access is
still needed; do not spin.
Inside the reviewed Gate-1 envelope, a direct create, update, grant, or
revoke call is also an access-request surface. If the exact Gate-2 grant is
missing or revoked, the host files one durable entity-access ask. Allowing it
lets the host reauthorize and retry that non-destructive leaf once. Schema
errors, unknown or missing types, host-only restrictions, constraint/scope
mismatches, and an active-but-insufficient grant are non-grantable failures;
they do not produce a broader permission card.
A direct create, get, query, update, delete, grant, or watch call
that instead finds its concrete type/operation missing from Gate 1 may open the
distinct host-owned Review changed access card for an editable scaffolded
project, but only when the live manifest already declares that exact pair.
Update access writes the exact Gate-1 entry and matching Entity grant,
reloads the project, and never replays the failed call; run it again.
entities.requestAccess remains Gate-2-only and cannot create this marker, add
a type/operation to Gate 1, or request a wildcard widening.
An Entity type may declare bounded privateContent slots. A slot names the
record field that carries its opaque content projections, exact accepted media
types, cardinality/item and byte limits, retention, optional flat metadata,
and a prompt-egress rule. This declaration is Gate 1 metadata only: it never
creates a grant, a Files folder, a URL, or a storage-provider selector.
Use the exact declared Entity permissions content-write, content-read, and
content-redact; old create, read, update, or wildcard grants do not
imply any of them. The SDK methods are entities.content.append,
entities.content.read, and entities.content.redact. Their local RPC values
carry a Uint8Array and an opaque content id; they never accept or reveal a
FilesContentRef, publicId, path, presigned URL, or blob locator. Browser
mock mode refuses these calls because it has no host-owned private content home.
On desktop, append commits an opaque projection into the declared Entity field and journals the host-only CAS mapping before making it readable. Reads recheck the exact content grant and live record on every delivery. Redaction removes logical reachability immediately; physical byte reclamation is asynchronous. Private mappings, receipts, and CAS selectors are excluded from Entity sync and backup snapshots, so restore invalidates them rather than manufacturing access.
Projects cannot choose an actor, owner, Member, Site User, tenancy, or content home in any private-content input. The host resolves those facts at the same authorization boundary as the target Entity. Hosted bulk upload/download is a separate carrier contract; do not tunnel large evidence through project JSON or invent an HTTP/Files fallback.
An entity call whose ask was allowed but whose host write then failed refuses as
entity operation denied: <method>: apply-failed, which maps to a typed
ProjectCallDenial with reason: 'apply-failed' and
actionTarget: 'host' — NOT 'permissions'. The grant was approved, so an
Access/permissions affordance is the wrong repair; say the host could not apply
the decision and offer a retry.
delete deliberately takes two invocations when its Gate-2 grant is missing.
The first may acquire the exact delete grant, then returns
access-updated-rerun-required without deleting. Run it again to receive the
separate record-bound destructive confirmation. The host reauthorizes the
grant immediately before mutation; denial, cancellation, expiry, and
revocation never delete the record.
Never seed fake records to disguise missing access. entities.grant/revoke and
privileged MCP entity-grant tools are not self-approval mechanisms.
Use optimistic versions for updates when concurrent edits matter. Keep query and watch scopes bounded. Too many standing watches reject with a quota error; lifecycle reap may resolve a long poll empty, which is normal.
Entities are local-first. A successful create, update, or delete means the
host committed the local record and its durable replication operation together;
query, watch, and subscribe observe that committed local projection without
waiting for Cloud. Cloud propagation is eventual and host-owned. Do not add a
project-side sync loop, poll a cloud endpoint, delay local success for network
reachability, or treat the bounded watch event feed as an audit/replication log.
If Cloud later refuses an operation, the host retains the local work and exposes
resolution through host sync status; projects do not delete or silently roll it
back.
A managed Telegram conversation reaches the cloud Assistant, which uses the same canonical Entities and exact standing grants. It does not run the local app or replicate arbitrary SQLite files, browser storage, or local secrets. Use the native entity schema, reviewed requests, and normal host sync path for records that should be available while the desktop sleeps.
App access and Assistant access are separate. Desktop setup selects an app; the host derives its reviewed, owned, registered types with ordinary userspace-backup eligibility and files exact Assistant read/query consent. Cloud type discovery lists only readable types, so it cannot bootstrap access to an ungranted type. Projects cannot seed grants, choose Assistant operations, or send type metadata to the cloud as a substitute for user consent. The host also requests exact conversation-history authority through its existing permission flow. Telegram asks separately for additional Data operations.
Cloud writes with an expected version are checked atomically at sequencer admission. Handle a stale version by rereading and resolving the change; never silently retry an overwrite. A cloud commit can precede desktop pull. If a later conversation-history write fails, the earlier Data change may still be committed: check the record before repeating the request.
Included from the installed project-agent guidance: data-and-authority.md, “Entities”.
import { entities } from '@ispo/sdk'
interface Note {
title: string
}
export async function readNotes() {
return entities.query<Note>('notes.note', {
limit: 20,
})
}
query reads a bounded snapshot and does not create or change records. Repeating a query may return a newer snapshot. An empty grant-filtered result means no records are visible to this caller, not that the database is empty. Show connecting, denied, empty, and error states separately. A missing type needs a valid declaration and host admission; retrying the same read cannot register it.
For live UI, use a subscription or the React guide; release the subscription when the view is disposed. A read timeout rejects the call; there is no per-query cancellation argument.
Typecheck against the installed SDK. In ISPO, read a known visible record, change it through an authorized action, and re-query. Check the denied and empty states with the same UI. Confirm that leaving a live view disposes its subscription.