@recursiv/sdk
v0.7.50
Published
TypeScript SDK for the Recursiv API
Readme
@recursiv/sdk
Infrastructure for AI agents and the apps that use them. Databases, deploy, file storage, AI agents with memory, auth, and social primitives. One SDK. Zero dependencies.
Install
npm install @recursiv/sdkNode.js >= 18. ESM only (import works on 18+; require() needs Node >= 22, which can require ES modules). Zero runtime dependencies; realtime and the Next.js helpers each declare one optional peer, installed only if you use them.
60-second quickstart
import { Recursiv } from '@recursiv/sdk'
const r = new Recursiv() // reads RECURSIV_API_KEY from env
// Provision a database
const { data: db } = await r.databases.ensure({ name: 'my-app', project_id: 'proj_1' })
const { data: creds } = await r.databases.getCredentials({ project_id: 'proj_1', name: 'my-app' })
console.log(creds.connection_string) // postgresql://...
// Create an AI agent
const { data: agent } = await r.agents.create({
name: 'Assistant',
username: 'assistant_1',
model: 'anthropic/claude-sonnet-4',
system_prompt: 'You are a helpful assistant.',
organization_id: 'org_xxx',
})
// Stream agent responses
for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' })) {
process.stdout.write(chunk.delta ?? '')
}What can you build?
| App type | Recursiv gives you | Example | |----------|-------------------|---------| | AI-native app | Database + AI agent + file storage + auth | Home maintenance tracker, AI CRM, smart inventory | | Autonomous agent | Agent with its own Postgres, memory, and code execution | Research agent, data pipeline agent, ops bot | | Social platform | Posts, communities, chat, profiles, follow graph | Team forum, community hub, social network | | Code playground | Sandboxes, deploy, AI coding assistant, live preview | Vibe coding tool, learning platform, code editor | | Any app that needs a backend | Auth + database + storage + billing | Skip Firebase/Supabase — one SDK covers it all |
Choose the right primitive
| Your app needs... | Use this | Not this |
|---|---|---|
| Structured data (users, records, tasks) | r.databases | r.posts with JSON blobs |
| Social content, feeds, discussions | r.posts + r.communities | r.databases for feeds |
| File uploads (photos, docs, media) | r.storage | r.uploads (legacy) |
| User auth and API keys | r.auth | Raw fetch to auth endpoints |
| AI agent with a model | r.agents | Direct LLM API calls |
| Measure agents (evals, accuracy, cost) + audit trail | r.evidence | Ad-hoc logging |
| Run code in a sandbox | r.projects.executeCode() | External sandbox services |
| Deploy to production | r.projects.deploy() | Manual CI/CD |
API overview
Infrastructure
| Resource | Key methods |
|----------|------------|
| r.databases | ensure, create, list, getCredentials, query |
| r.envVars | set, list, delete (values write-only; secrets masked on read) |
| r.projects | create, deploy, executeCode, createSandbox, stopSandbox, deploymentLogs |
| r.storage | ensureBucket, getUploadUrl, getDownloadUrl, listItems, deleteObject |
| r.sandbox | execute (anonymous, no API key needed) |
AI Agents
| Resource | Key methods |
|----------|------------|
| r.agents | create, update, chat, chatStream, grantProjectAccess, conversations |
| r.e2ee | registerDevice, devices, revokeDevice, publishKeyPackages, keyPackageStatus, claimKeyPackage, appendEvent, events, acknowledge (flag-gated E2EE v2 transport) |
| r.brain | sendMessage |
| r.commands | execute, gatePrompt |
| r.projectBrain | tasks, decisions, milestones, completeTask, usage |
| r.metrics | get, forOrg, forProject, children, definitions — the KPI ledger per tier (platform, network, org, project): numbers with definitions, status, and confidence |
Identity & Auth
| Resource | Key methods |
|----------|------------|
| r.auth | signUp, signIn, getSession, signOut, createApiKey (session reads/sign-out use explicit Bearer auth with a native/server Cookie fallback) |
| @recursiv/sdk/next | createRecursivAuth — cookie-session server auth for Next.js App Router (see below) |
| r.users | me, get |
| r.organizations | create, members, addMember, invite |
| r.profiles | me, get, search, follow, unfollow |
Social
| Resource | Key methods |
|----------|------------|
| r.posts | create, update, list, liked, search, react |
| r.communities | create, join, members |
| r.chat | send, dm, createGroup, messages |
| r.tags | create, list |
| r.uploads | uploadMedia — upload post/chat bytes end to end |
Platform
| Resource | Purpose |
|----------|---------|
| r.settings | User preferences (including app-scoped content choices), sessions, password management |
| r.billing | Usage tracking, subscriptions, checkout |
| r.appSubscriptions | Consumer app plans: checkout, status, createPortalSession |
| r.notifications | Push notification tokens |
| r.github | GitHub integration |
| r.integrations | External service connections |
| r.admin | User management, stats, content reports (listReports, resolveReport, dismissReport) |
| r.dispatcher | Task queue, claims, signals, outcomes, webhooks |
| r.goals | Org goals with task rollups (group dispatcher tasks) |
| r.protocols | Private Minds safety: sourceControls (current enablement and saved choices), setSourceControl, onSourceControlChange, reportSource. Delegated accounts: nativeSocialAccounts, startNativeSocialConnection, finishNativeSocialConnection, disconnectNativeSocialAccount. Native follows: nativeFollowState, prepareNativeFollow, submitNativeFollow, nativeFollowStatus (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: nativeRepostState, prepareNativeRepost, submitNativeRepost, nativeRepostStatus (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: nativeLikeState, prepareNativeLike, submitNativeLike, nativeLikeStatus (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: prepareNativeReply, submitNativeReply, nativeReplyStatus, nativeReplyHistory, prepareNativeReplyRemoval, removeNativeReply (require deployed API support and app enablement). Keyless shared post links: sharedNetworks(projectId), getSharedConversation(projectId, { protocol, post, readRef? }). Member public reading: searchPublicPosts (selected account required for Bluesky/ActivityPub; public Nostr search), getConnectedSocialConversation (preserve result readerAccountId/readRef), publicNetworks, searchPublicProfiles, getPublicProfile, getPublicAuthorFeed, getFollowingFeed, getPublicConversation, getPublicTrends, getPublicTrendFeed; private app subscriptions: listSubscriptions, subscribeToProfile, unsubscribeFromProfile, onSubscriptionChange. list returns bounded adapter maturity, limitations, and exhaustive supported/planned/unsupported implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via listCandidates / getCandidate |
Full reference: see llm.md (ships with this package) or the type definitions in dist/.
Quarantined protocol candidates
r.protocols.listCandidates({ protocol?, limit?, cursor? }) and
r.protocols.getCandidate(id) read unpublished candidates, not feed posts.
Both require an admin-scoped, project-bound key, a live admin role, and
existing project-admin access. App/network scope is not a caller parameter.
These GET methods do not collect content, update settings, or publish anything.
Lists return { data, meta: { limit, has_more, next_cursor } } with no offset;
the API defaults to 20 rows and permits at most 50. Details return { data }.
Render candidate content as plain text and respect
content_truncated. Safe source IDs are preserved byte-exact. A null
external_id means unsafe or malformed provenance was withheld; an author's
native_id may likewise be absent or withheld. Do not invent replacement IDs.
API failures, including routes absent from an older
deployment, throw the normal SDK errors; do not present them as an empty queue.
Configuration
const r = new Recursiv({
apiKey: 'sk_live_...', // or set RECURSIV_API_KEY env var
baseUrl: 'https://api.recursiv.io/api/v1', // default
timeout: 30000, // request timeout in ms
maxRetries: 2, // auto-retry on 429/5xx
})
// Zero-arg works when RECURSIV_API_KEY is set
const r = new Recursiv()
// Anonymous sandbox (no API key needed)
const r = new Recursiv({ anonymous: true })
// Self-hosted
const r = new Recursiv({ baseUrl: 'https://my-instance.com/api/v1' })Projects & deploy
// Deploy to production
const { data: deployment } = await r.projects.deploy('proj_1', {
branch: 'main',
type: 'production',
})
// Execute code in a sandbox
const { data: result } = await r.projects.executeCode('proj_1', {
code: 'console.log("hello")',
language: 'typescript',
})
console.log(result.output) // "hello\n"
// Get deployment logs
const { data: logs } = await r.projects.deploymentLogs('proj_1', deployment.id)
// Start/stop sandboxes
await r.projects.createSandbox('proj_1')
await r.projects.stopSandbox('proj_1')AI agents
// Create an agent
const { data: agent } = await r.agents.create({
name: 'Research Bot',
username: 'researcher',
model: 'anthropic/claude-sonnet-4',
system_prompt: 'You help users find information.',
tool_mode: 'autonomous',
organization_id: 'org_xxx',
})
// Chat (async — agent responds asynchronously)
const { data: reply } = await r.agents.chat(agent.id, {
message: 'What are the latest trends in AI?',
})
// reply: { message_id, conversation_id, content, created_at }
// Stream (token-by-token)
for await (const chunk of r.agents.chatStream(agent.id, {
message: 'Tell me more about transformer architectures',
})) {
process.stdout.write(chunk.delta ?? '')
}
// Give agent access to project infrastructure
await r.agents.grantProjectAccess(agent.id, {
project_id: 'proj_1',
permissions: ['execute_code', 'read_files', 'write_files'],
})Social primitives
// Posts
const { data: post } = await r.posts.create({
content: '# Shipped!\n\nNew deploy is live.',
content_format: 'markdown',
community_id: 'comm_abc',
})
// Post/chat media: the SDK obtains the signed URL, PUTs the bytes, checks
// every response, and returns the public URL.
const mediaUrl = await r.uploads.uploadMedia({
blob: imageFile,
contentType: imageFile.type,
})
// Chat
const { data: dm } = await r.chat.dm({ user_id: 'user_abc' })
await r.chat.send({ conversation_id: dm.id, content: 'Hello!' })
// Communities
const { data: community } = await r.communities.create({
name: 'TypeScript Devs',
slug: 'ts-devs',
privacy: 'public',
})Real-time Chat (React / React Native)
The SDK provides a ready-to-use WebSocket hook for building chat applications without REST polling.
import { Recursiv } from '@recursiv/sdk'
import { useChat } from '@recursiv/sdk/react'
const r = new Recursiv()
function ChatRoom({ conversationId }) {
const { isConnected, error, sendTyping, sendMessage } = useChat(r, {
conversationId,
onMessage: (msg) => {
console.log('New message received:', msg.text)
},
onTyping: (evt) => {
console.log(`${evt.userName} is typing...`)
},
onAgentThinking: (evt) => {
console.log(`Agent ${evt.agentName} status: ${evt.status}`)
}
})
// Automatically connects, joins room, and listens to events!
return (
<div>
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
<button onClick={() => sendMessage('Hello world!')}>Send</button>
</div>
)
}Customer-app password auth
On a shared Recursiv API hostname, pass the customer app's project when signing in and minting a per-user key. The SDK forwards that project on the password sign-in request so the server resolves same-email users in the app's network.
const anonymous = new Recursiv({ anonymous: true, baseUrl })
const { apiKey, user } = await anonymous.auth.signInAndCreateKey(
{ email, password },
{
name: 'my-app',
scopes: ['posts:read'],
projectId: 'prj_xxx',
consumeSession: true,
},
)consumeSession: true atomically retires the temporary login session as the
API key is minted. Use it when the customer app stores and authenticates only
with the returned API key; omit it for flows that still need the cookie session.
Next.js server auth (@recursiv/sdk/next)
Cookie-session helpers for the App Router (Next 14 and 15). Each user signs in once, gets a project-scoped per-user API key in an httpOnly cookie, and every request acts as that user — no platform key on the server.
This subpath uses the optional server-only peer to keep its exports out of Client Components. Install it alongside the SDK in Next.js apps: npm install server-only.
// src/lib/recursiv.ts (server-only module)
import 'server-only';
import { createRecursivAuth } from '@recursiv/sdk/next';
export const auth = createRecursivAuth(); // RECURSIV_PROJECT_ID from env
// src/actions/auth.ts ('use server')
export async function signIn(input: { email: string; password: string }) {
const { user } = await auth.signIn(input); // mints key + sets session cookie
return user;
}
// any server component or route handler
const sdk = await auth.getSdk(); // Recursiv acting as the signed-in user
const { data: me } = await sdk.users.me();Route handlers building their own response pass a cookie writer, and the cookie contract (name, lifetime, attributes) is fully configurable:
const auth = createRecursivAuth({ cookie: { name: 'myapp_session', maxAge: 60 * 60 * 24 * 90 } });
export async function POST(req: NextRequest) {
const res = NextResponse.json({ ok: true });
await auth.verifyOtp(await req.json(), { response: res.cookies });
return res;
}Server-side only — the server-only guard fails the build if it's ever imported into a client component. Keep it out of middleware.ts as well (edge bundle size); gate routes with a plain cookie-presence check on a shared constant.
Error handling
import {
Recursiv,
AuthenticationError, // 401 — missing or invalid API key
AuthorizationError, // 403 — API key lacks required scope
NotFoundError, // 404
ValidationError, // 400 — check error.details
RateLimitError, // 429 — includes retryAfter, upgradeUrl
ConflictError, // 409
} from '@recursiv/sdk'
try {
await r.projects.deploy('proj_1', { branch: 'main' })
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`Retry after ${err.retryAfter}s`)
}
}Auto-retry is built in. 429 and 5xx responses retry with exponential backoff (configurable via maxRetries, default 2).
Pagination
const page1 = await r.posts.list({ limit: 20 })
if (page1.meta.has_more) {
const page2 = await r.posts.list({ limit: 20, offset: 20 })
}Response shapes
{ data: T[], meta: { limit, offset, has_more } } // list
{ data: T } // single
{ data: { deleted: true } } // delete
{ data: { success: true } } // successWorks everywhere
| Environment | Status | Notes |
|-------------|--------|-------|
| Node.js >= 18 | Full support | ESM only (require() needs Node >= 22). Uses native fetch. |
| Next.js | Full support | Server components, server actions, and route handlers. Use @recursiv/sdk/next for cookie-session auth (Next 14 and 15). |
| React (Vite, CRA) | Full support | Bundle-friendly, zero dependencies. Realtime needs the optional socket.io-client peer. |
| React Native / Expo | Supported with caveats | chatStream() requires manual SSE handling. See React Native guide. |
| Deno | Full support | ESM native, fetch native |
| Bun | Full support | ESM native, fetch native |
| Browser (direct) | Full support | Uses native fetch. CORS must be configured for your domain. |
AI assistant reference
This package includes llm.md — a complete reference designed for AI coding assistants (Claude, Cursor, Copilot). It covers all resources with decision rules, working examples, and known gotchas. Your AI assistant will read it automatically when it encounters this SDK.
Types
All request and response types are exported:
import type {
Project, Deployment, DeployInput, ExecuteCodeInput,
Agent, StreamChunk, AgentChatInput,
Post, Community, Message, Conversation,
PaginatedResponse, SingleResponse,
} from '@recursiv/sdk'Guides
- Build an AI-native app — Database + agent + storage + auth
- Build a social platform — Posts, communities, chat, profiles
- Build a code playground — Sandboxes, AI coding, deploy
- Give an agent its own infrastructure — Database + memory + code execution
- Mobile / React Native setup — Expo, streaming workarounds, auth flow
- Error reference — Every error type, what causes it, how to fix it
Links
License
FSL-1.1-ALv2 — source-available, converts to Apache 2.0 after 2 years.
Public source trends
Check publicNetworks() for public_trends before calling
getPublicTrends({ protocol, limit: 10, cursor }). The response is a discriminated
union: kind: 'topics' contains Bluesky topics; kind: 'posts' contains public
ActivityPub posts ranked by mastodon.social. Preserve provider order and display
provider, observedAt, and coverage: 'provider_ranked_window'. A null
rankingWindow means the provider has not supplied an exact ranking interval.
For a network with trend_feed, open a returned topic with
getPublicTrendFeed({ protocol, topic: topic.nativeId, limit: 10, cursor }).
The service rechecks current topic availability before each page; a disappeared
topic returns trend_unavailable rather than an empty feed. Keep canonical post
IDs and provider readRef when opening getPublicConversation. Cursors are
opaque; topic catalogs have no pagination and the Mastodon post window stops at
200 source positions. Both reads require users:read and posts:read on the
current human project-bound app key. Nostr does not advertise trends. Provider
failures propagate as SDK errors, and neither method ingests or publishes posts.
Native Nostr signing
Use protocols.nativeNostrAccount() to check the app's explicit native-write
capability. For an enabled connected key, prepare a text post with
prepareNativeNostr(identityId, { action: 'publish', content }), show its
identity, relay and text, and ask the client signer to sign the returned event.
Submit only the public event with submitNativeNostr(identityId, { intent, event,
approve: true }). Preserve that exact event on uncertain retries. Check
getNativeNostrPost(identityId, eventId) for relay visibility. An owned note can
prepare { action: 'delete', postId }; deletion is a signed request, not a
promise of erasure across all relays. Keys are never sent to this SDK service.
Preparation and submission never retry automatically, even when maxRetries is
configured. After an uncertain submission, check
getNativeNostrPost(identityId, preview.postId ?? preview.eventId): deletion uses
the original note's postId, while publication uses the new eventId. Any
user-selected retry must keep the same intent and signed event.
When the deployed API advertises reply_text_note, the same method can prepare
{ action: 'reply', content: draftText, parentId: selectedPost.nativeId }. Review
the returned parent, root, participants, relay and exact event before asking the
signer to sign. The API constructs verified NIP-10 references; callers cannot
inject tags, participant lists, relay URLs or signing keys. Reply prepare/submit
also require posts:read. This capability remains off until the shared reply UI
and live delivery are verified. Only public kind-1 notes are supported; additional
participant selection is unfinished. textTruncated labels an abbreviated source
preview. Preparation and submission never automatically retry; check the event ID
after an uncertain result before choosing a retry of the identical signed event.
Delegated Bluesky and ActivityPub accounts
These methods require the same signed-in human app key throughout. The API must enable the app and deploy the authorization schema before use.
const { data: connection } = await r.protocols.startNativeSocialConnection({
protocol: 'bluesky', identifier: 'alice.bsky.social',
})
// Navigate to connection.authorizationUrl. On the configured app callback:
const { data: account } = await r.protocols.finishNativeSocialConnection({
protocol: 'bluesky', state, code, iss,
})
const { data: accounts } = await r.protocols.nativeSocialAccounts()
await r.protocols.disconnectNativeSocialAccount(account.id)ActivityPub uses an operator-approved HTTPS server origin as identifier and
requires Mastodon-compatible OAuth metadata with S256 PKCE. The server configures
the exact callback path /social/connect/callback/:protocol; the app must remove
its query from browser history immediately and never log codes. No password or
native token is sent through these SDK methods. Start and callback are single-use
requests and are never automatically retried. If a callback result is uncertain,
refresh the account list before starting again.
Connection does not approve publication. The account's capabilities lists only
implemented actions, currently empty for these adapters. Local disconnect takes
effect even when providerRevocation is unconfirmed; the user can also revoke
Minds from their network's authorized-app settings.
One reviewed reply across selected accounts
The shared reply API is off by default. Deploy its required schema/API and finish live authorization and delivery verification before enabling it. This example prepares a preview; it does not publish anything:
const { data: preview } = await r.protocols.prepareNativeReply({
clientMutationId: crypto.randomUUID(), text: draftText,
parent: { protocol: 'bluesky', nativeId: selectedPost.nativeId },
accountIds: [blueskyAccount.id, activityPubAccount.id],
linkedPostAccountIds: [activityPubAccount.id],
// Optional, needs each account's `publish_media` capability:
media: [{ url: uploadedImageUrl, alt: memberWrittenAltText }],
})
// Review each delivery's account, protocol, kind, parent, media and exact wireText.
// After the user approves selected delivery IDs:
await r.protocols.submitNativeReply(preview.id, { approve: true, deliveryIds: approvedDeliveryIds })Use linkedPostAccountIds only after the user selects a standalone post with the
source link on that different network. It is not a matching threaded reply, and
a denied same-network reply never falls back automatically. Verified cross-network
thread mappings are not implemented here. Nostr uses the connected client signer
described above. Preserve the mutation
ID while recovering an uncertain preparation; read nativeReplyStatus(preview.id) to
show each destination's result. Repeated submit reconciles an attempted delivery
without publishing it again. Removal uses a fresh prepareNativeReplyRemoval
preview and explicit removeNativeReply approval for the selected own posts.
For a selected unsent or definitively rejected destination, the same prepare
method accepts recoverFrom: { batchId: preview.id, deliveryIds: selectedIds }
with a new mutation ID, the same source/accounts and freshly reviewed text.
Preserve that new ID if preparation is uncertain. The server creates the fresh
review and disables only those old destinations atomically; replacedBy points
to the new batch. recoverable describes saved eligibility, while current account,
source and safety checks still apply. Sending, unknown, accepted or published
destinations cannot be resent this way. The fresh preview still needs explicit
approval; completed destinations retain their outcomes.
nativeReplyHistory pages through one private history containing Bluesky/ActivityPub
drafts and delivery results, plus Nostr replies and linked posts once submitted. It also includes unsent or expired preparations. The
summary contains a bounded preview of the user's own text and source references,
not old remote parent text or approval material. Opening a result with
nativeReplyStatus(id) does not send anything. Reading remains available when
new replies are disabled; it does not refresh credentials or contact a network.
Nostr items have type: "nostr". nativeNostrReplyResult(id) checks the saved event
on its recorded relay without signing or resending. It requires the same connected
identity and preserves historical publication/deletion evidence separately from
lastReadBack. A relay absence is not proof of global deletion. Signatures and
intents are never retained in history.
const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })
// Read a saved result without submitting it.
const firstResult = history.items[0]
if (firstResult) {
if ('type' in firstResult && firstResult.type === 'nostr') {
await r.protocols.nativeNostrReplyResult(firstResult.id)
} else await r.protocols.nativeReplyStatus(firstResult.id)
}
if (history.cursor) {
const { data: next } = await r.protocols.nativeReplyHistory({ limit: 20, cursor: history.cursor })
console.log(next.items.length)
}Keep the cursor opaque and discard it when the app or signed-in user changes. History order is fixed by creation time, so later delivery updates do not move older entries across page boundaries. Reopen each result for its current saved state and obtain a fresh review before a new write.
Public protocol post links
Visitors can read a public conversation in an explicitly configured app without signing in:
const visitor = new Recursiv({ anonymous: true });
const { data: networks } = await visitor.protocols.sharedNetworks(projectId);
const { data: conversation } = await visitor.protocols.getSharedConversation(projectId, {
protocol: 'nostr', post: publicEventId,
});These reads apply app moderation, provider public-visibility/deletion checks and bounded
per-IP/app budgets. They return no subscriptions, personal safety preferences, account
connections or signing authority. Missing configuration or unavailable safety checks fail
closed. Signed-in members continue using getPublicConversation to apply personal
blocks/mutes as well. All reply and account endpoints still require authentication.
Standalone public publication uses protocols.prepareNativePost({ clientMutationId, text, accountIds, media? }), then explicitly approved submitNativePost. Keep the same mutation ID after uncertain preparation; previously attempted deliveries are reconciled without resending. nativePostStatus and paginated nativePostHistory expose private app/user-owned outcomes. Review current own posts with prepareNativePostRemoval, then approve exact removal IDs through removeNativePost. Check connected-account publish_text capabilities; only explicitly enabled apps may publish. Text is sent unchanged, public only, with each provider’s actual limits. Optional media is a NativeMediaInput[] of at most four already-uploaded Minds images ({ url, alt?, width?, height?, mimeType? }, JPEG/PNG/WebP/GIF, Minds-hosted HTTPS URLs only, a Mastodon GIF must be the only attachment). It additionally requires the account's publish_media capability; accounts connected before image permissions existed must reconnect. Pass the member's own alt — an omitted one is sent as an empty description, never an invented one — and read receipt.media ({ count, alt_missing }) after sending. These methods still do not accept a reply parent or caller-supplied authority.
For Bluesky accounts advertising quote, prepare the same publication with quote: { protocol: 'bluesky', nativeId: sourceAtUri }. The server resolves the exact source CID using each connected account, checks quote permission and Minds safety, and rechecks before writing. Review each delivery’s quote alongside the account and comment before submitting. Changed or unavailable sources cannot publish; uncertain deliveries reconcile the original record key. ActivityPub/Nostr quotes are not provided by this method. Public post reads include at most one filtered quote with state: 'available' and post, or state: 'unavailable' without source content. Available quotes may include embeddedReferences: exact standalone source-reference lines in the unchanged outer text. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
Topic browsing can request getPublicTrends({ protocol: "bluesky", includePreviews: true }). Optional SocialTrend.preview is a real image-bearing post from that topic, never a generated illustration. The server samples at most eight posts for each of ten topics, with three concurrent reads and a shared 2.5-second deadline; missing images leave usable text topics. App moderation and user source controls filter previews on every request. Bluesky CDN image attachments, quote-with-media images, and unlabeled link-preview thumbnails are supported; omitted content retains the source link.
Continuous public discovery
Create one sdk.discovery.createFeed({ networks, category, source, learning })
per signed-in viewer and filter. Pass the app-advertised network catalog, then call
next() as the reader approaches the end. Each page contains safe json-render
module specs and native or protocol post objects. Preserve their existing source
authority and interactive controls. retryFailed() explicitly retries unavailable
sources; create a new session to refresh exhausted source windows.
Optional DiscoveryLearning consumes real visible impressions and explicit
engagement/hide outcomes. Persist its count-only snapshot per viewer; never share
a session or learning state across accounts. It starts conservatively and does
not imply measured retention improvement.
