ISPO SDK
    Preparing search index...
    Markdown

    Query entities

    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.