Cya App Platform · API v1

Build experiences people share together.

Build multiplayer games, AI agents, collaborative workspaces, 3D worlds, interactive media, and experiences nobody has invented yet. Cya keeps every participant perfectly synchronized in real time while handling state, permissions, storage, networking, and distribution behind the scenes.

No backend required
Stop building distributed systems. No WebSockets, presence, or conflict resolution to maintain — Cya handles all of it. You just build the experience.
Perfectly synchronized
Five people or a stadium, everyone sees the same state in real time. Every interaction is ordered and instantly shared, so no one drifts out of sync.
Secure by default
Apps run in isolated sandboxes with capability-based permissions. Request only the access you need — nothing more.
Deploy with confidence
Every release is validated, reviewed, versioned, and safely distributed. Your users always run a version you approved.
Concepts

How it works

You build a normal web app. Cya runs it for everyone at once and keeps them in sync. A room is simply where that shared experience happens — a live session with a host, promoted participants, and an audience, from a few people to thousands. When the host launches your app, every participant's client mounts your bundle in a sandboxed iframe and hands it a session token over the Cya App Bridge (CAB, a typed postMessage contract). From there, the SDK is your entire backend:

the moving parts
┌─ Cya Room (host + audience) ─────────────────────────────┐
│  ┌─ your iframe: <appId>.apps.cya.live/<version>/ ──────┐  │
│  │  @cya/app-sdk                                        │  │
│  │   ├─ app.sync    documents · signals · counters      │  │
│  │   ├─ app.roles   participants · consent invites      │  │
│  │   ├─ app.storage R2 assets + JSON KV (quota'd)       │  │
│  │   └─ app.clock   server clock for drift-free timing  │  │
│  └───────────────────────────────────────────────────────┘  │
│        ▲ CAB handshake + 5-min capability tokens            │
└──────────────────────────────────────────────────────────────┘

Every write lands on one server-side instance and fans out to all participants in order, so everyone converges on the same state — late joiners included. You don't stand up servers, sockets, or a database, and assets get server-enforced storage out of the box. Building synchronized software feels like building a normal web app.

Get started

Quickstart

terminal
npm i -g @cya/cli
cya app init my-app     # scaffold with @cya/app-sdk wired up
cd my-app && npm install
cya app dev             # local host simulator

One call. The SDK handles the handshake, token refresh, and reconnection:

src/main.ts
import { connectCyaApp } from "@cya/app-sdk";

const app = await connectCyaApp();      // CAB handshake → ready
const { self, event } = app.context;
console.log(event.eventId, self.userId, self.platformRole);

// shared, synchronized state — every participant sees this
const game = app.sync.doc("game");
game.on("change", ({ value }) => render(value));
if (app.roles.has("host")) {
  await game.set({ round: 1, phase: "lobby" });
}

Validate, bundle and submit:

terminal
cya check               # manifest + scope lint (same checks as review)
cya pack                # produce the uploadable bundle
# then: cya publish, or upload via the developer portal UI

Prefer the UI? The developer portal mirrors the CLI: add your listing, icon, and screenshots, paste your manifest, pick your built dist/ folder, and submit. Build with a relative base (vite base: './') — bundles are served under a versioned path.

API reference

Lifecycle & tokens

connectCyaApp() resolves after the CAB connect → init → joined handshake with a ready CyaApp. The host may pause, resume or tear your app down at any time.

app.contextAppContext — self (userId, displayName, roles, platformRole), event.eventId, appId, instanceId, slot.
app.auth.token()Current session token (auto-refreshed ~5 min); attach as Authorization: Bearer for direct HTTP calls.
app.on('pause' | 'resume' | 'teardown', fn)Host lifecycle events. Free your timers on pause; teardown is final.
app.clock.now()Server clock (ms). Use for countdowns and sync so late joiners agree.

Tokens are scoped to your manifest and expire in ~5 minutes; the SDK refreshes them for you. Nothing your app can do outlives the session.

API reference

Synchronized state

Four primitives, all synchronized for you. Writes are ordered server-side and every participant converges on the same state — late joiners included, via snapshot plus replay. This is the core of Cya: shared state that just works.

Documents — shared mutable state

app.sync.doc(name)A named JSON document for this app instance (per-user docs via doc(name, ofUserId)).
doc.value / doc.revCurrent value and revision.
doc.set(patch, { ifRev })Dot-path patch with optional compare-and-set. Rejected CAS → re-read and retry.
doc.on('change', fn)Fires for local and remote changes with { value, rev }.
optimistic concurrency
const game = app.sync.doc("game");
try {
  await game.set({ "players.u42.score": 10 }, { ifRev: game.rev });
} catch {
  // someone wrote first — doc.value is already fresh, retry your patch
}

Signals — fire-and-forget events

app.sync.signal(name).send(data)Broadcast a transient event (buzz, emoji, ping). Not persisted.
signal.on(fn)Receive { by, data, seq } in order.

Counters — server-tallied numbers

app.sync.counter(name).incr(by)Atomic increment (votes, scores) — no read-modify-write races.
counter.value / counter.on('change', fn)Live tally.

Playback — synced media clock

app.sync.playback(name)Host-driven play/pause/seek state machine on the server clock.
playback.play(fromMs) / pause() / seekTo(ms)Grant-holder controls (manifest write policy); viewers follow.
playback.position()Derived position on the server clock — no per-tick sync traffic.
driftAction(actualMs, targetMs)Tells you to rate-nudge or hard-seek your media element to stay in sync.
API reference

Roles & consent

Promote participants into roles — contestants, co-hosts, panelists. Promotion is consent-based: invite() asks, the user accepts in Cya's own UI, and only then does the grant land (assign() grants directly where your policy allows it). Role-scoped channels carry private state, like a contestant's hidden prompt.

app.roles.selfYour app roles + epoch.
app.roles.membersuserId → roles for the instance.
app.roles.has(role)True for your app roles; 'host' also honors the platform host.
app.roles.invite(userId, role)Consent flow — the user accepts in Cya's own UI before the grant lands.
app.roles.assign(userId, role)Direct grant, no prompt (where your manifest policy allows).
app.roles.revoke(userId, role) / leave(role)Remove a grant; epochs bump so stale writers are fenced.
app.roles.on('assigned' | 'revoked', fn)Live membership changes.
API reference

Storage

R2-backed assets and a small JSON KV, scoped per app and per user with server-enforced quotas. Declare the scopes you need:

userPrivate to one user, follows them across rooms (profiles, save-games).
room:selfOne participant's data within one room (their answer sheet).
room:sharedShared room data — the HOST's quota pays for it (uploaded photos, board state).
app.storage.upload(file, { scope, path })Two-phase upload (presigned PUT + confirm). Returns the stored { key, bytes }.
app.storage.get(key)Fetch an asset as a Blob (time-limited signed reads).
app.storage.putJSON(key, value) / getJSON(key)KV for small JSON (≤64 KB per entry); optional { scope }, default 'user'.
app.storage.list({ scope, prefix })Enumerate confirmed objects.
app.storage.delete(key)Remove an object and release its bytes.
app.storage.refreshUsage()Fetch bytes used vs quota from the server.
app.storage.quota / on('quota', fn)Cached usage snapshot + quota events as you approach limits (80/95/100%).
Reference

The manifest (cya.app.json)

The manifest is your capability contract. A namespace exists on the SDK only if you declare it, and the server enforces the same set on every call (E_SCOPE otherwise). Reviewers see exactly what you ask for.

cya.app.json
{
  "appId": "my-poker-app",
  "name": "Poker Night",
  "apiVersion": "1",
  "entry": "index.html",
  "policy": {
    "docs":     { "game": {}, "hand": { "perUser": true } },
    "roles":    { "player": { "max": 6 } },
    "signals":  ["buzz"],
    "counters": ["pot"]
  },
  "storage": { "scopes": ["room:shared", "room:self"] }
}
appIdPermanent slug; names your sandbox origin and storage folders.
apiVersionPlatform API line (currently "1").
entryBundle entry file, served at /<version>/<entry>.
policy.docsNamed documents (+ perUser / role visibility options).
policy.rolesRoles your app may grant, with member caps.
policy.signals / policy.countersNames you may send / tally.
storage.scopesStorage scopes (deny-by-default).
Reference

Limits

Session token~5 min TTL, auto-refreshed by the SDK; capability-scoped.
Documents≤64 KiB per doc · ≤128 docs per instance · ≤1 MiB total.
Event replayLast 256 events per channel replayed to late joiners (plus snapshot).
KV entries≤64 KB JSON each.
Listing imagesIcon + up to 8 screenshots, ≤5 MB each (PNG/JPEG/WebP).
Storage quotaPer-user quota shared across Cya (uploads reserve, confirm settles).
Shipping

Review & publishing

Submission runs automated checks first (blocking):

  • manifest schema + scope lint (undeclared API usage fails)
  • bundle sanity — entry file present, no root-absolute asset paths
  • size and content-type limits

Then Cya runs your app in a staging room with synthetic participants — lifecycle, role flows, and storage. Track status in the portal submitted → in review → approved / rejected. Approved versions go live in the Apps Center; creators install once, and your app reaches every audience they bring. New versions ship through the same pipeline while your current one keeps serving.

Build something people share.

Scaffold with the SDK, run it locally, and submit from the portal.