Sloppy Voice DevelopersSloppy Voice ↗

V1 protocol profile

Document: SPEC-009. Revision: 0.10 Date: 2026-10-21. Lifecycle: Approved for implementation under delegated decision D-024. Supersedes: draft 0.3 OPEN protocol mechanisms only for the V1 scope described here; later milestones retain their existing status.

The current /api/info response also includes a public build string identifying the released software version. It is informational, grants no authority, and is checked after deployment; it is not a substitute for profile/capability validation.

D-104/D-105 and SPEC-012 supersede flat ordinary channels and the always-PTT client restriction for channel-hierarchy/1 and ptt-policy/1 services. SPEC-004 defines personal PTT precedence and explicit voice activation; existing encrypted media, authority, revision and routing contracts remain unchanged.

Developer resource and control extensions (D-227/D-228)

SPEC-028 specifies additive logical-servers/1 and channel-control-messages/1 profiles, the published library and developer documentation. It supersedes the assumption that every accepted ordinary-channel message appears in the default history projection. All other signed-envelope, authentication, encryption, current-recipient and historical-eligibility rules remain in force.

Direct voice and relay fan-out (D-086)

D-086 and SPEC-017 define voice-fanout/1, signed common frames, recipient-selective one-copy relay uploads, addressed voice-peer/1 lease exchanges and peer authority snapshots. This supersedes the single-target media wire profile for updated calls. Capability rejection requires refresh/rejoin; a dual-profile native relay is permitted for ordered rollout only. Existing text/account wire contracts remain.

Conversation extension — 2026-10-19

SPEC-013 adds optional signed/encrypted replyTo metadata and isolated thread-call resolution. Its envelope field extension supersedes the original exact field list only when replyTo is present; all existing cryptographic and media boundaries remain applicable.

Contacts extension — 2026-10-19

SPEC-014 adds the contacts/1 capability, account-private contacts endpoints and optional contacts synchronization field. These additions do not enable federation or public registration.

Active extensions

SPEC-016 adds voice-keys/1, signed public key offers on voice leases and a versioned AEAD wrapper inside the existing relay ciphertext field. It supersedes static encrypted voice for updated clients/hosts, while retaining text envelopes and the relay routing/lease contract. Incompatible encrypted voice clients receive upgrade-required errors; no static-key downgrade is permitted.

SPEC-010 (D-058) supersedes the root-only service location and unresolved discovery mechanism. SPEC-011 (D-059/D-060) supersedes the one-device recipient/login restriction and adds signed device approval, eligible history sharing and password-plus-key recovery. SPEC-012 (D-055/D-058) supersedes the singleton-room restriction and schema 1 with scoped rooms and immutable encryption variants. Unchanged V1 authentication primitives, permission rules, retention and relay enforcement remain active. Legacy lobby payloads remain compatible until a roster change requires an upgraded client. Do not use a historical restriction below to override its active successor.

Authority and scope

D-024 records explicit delegation to resolve the remaining first-build choices without changing D-001–D-023. This profile defines voice-chat/1, an experimental single-domain protocol. It is not Matrix or XMPP compatible. The prior comparison is retained in the first-build review. V2 must preserve domain-qualified identities and direct room-host connections; V1 rejects foreign authentication rather than pretending federation works.

SPEC-005 owns clients and SPEC-008 owns server processing. SPEC-003 owns trust/encryption, SPEC-004 owns audio and SPEC-006 owns durability/history. Their revision 0.4 V1 sections supersede their draft mechanisms only for implemented V1 behaviour. This profile is a protocol declaration, not a deployment-selectable algorithm menu. A successful developer build does not establish V1 human/platform acceptance.

D-032 renames the product to Sloppy Voice (sloppy-voice). The existing voice-chat/1 wire identifier is retained for interoperability with the first alpha; branding is not a protocol upgrade.

D-044–D-046 request a successor with linked devices, recovery and explicit voice handover. This document remains authoritative for shipped voice-chat/1; its one-device authentication and ciphertext formats are not silently extended. The linked-device review identifies the required version/capability negotiation, trust, envelope and storage migration before that successor can be implemented. D-047's input recorder is client-local under SPEC-005 and cannot make a legacy client support the successor's account protocol.

D-035 selects Apache 2.0 for this specification and original reference code. Independent protocol implementations may use any licence, including proprietary licences; implementing this profile does not require adopting Apache 2.0. See SPEC-000's licensing policy for scope, reuse and patent terms.

Transport, identity and compatibility

D-056/D-057 confirm qualified account presentation and preserve this installation's sloppyvoice.com identity. SPEC-003 proposes the input-normalization adapter; it keeps this profile's canonical local username fields and proof serialization, verifying a supplied suffix before stripping it. SPEC-010 proposes the separately advertised identity-discovery/1 capability and path-capable base URL. These are not capabilities of deployed alpha.5. Do not change old proof bytes, account IDs or root routes implicitly; require the discovery extension before relying on its added /api/info fields or mount behavior.

D-055/Q-47 approve text/voice/combined room scope, with owner-only creation. The review defines observable requirements, but exact additive room capability names, signed payload variants, command shapes and versioned storage migration remain to be specified before application changes. Legacy unscoped routes remain confined to lobby.

Use same-origin HTTPS JSON APIs under /api/. A separately hosted client must be explicitly allowed by the service. Bearer login credentials are sent only to the configured control origin. Cross-origin browser access uses exact origin matching, no cookies, and no wildcard credentials. HTTP is permitted only for loopback development. Forwarding tokens have a different purpose and audience from login and command credentials.

Usernames are lowercase ASCII matching [a-z0-9][a-z0-9_-]{2,31}. Stable account and device IDs are UUIDs, separate from display handles. The public account identity combines the username and configured canonical domain. The V1 room is lobby at that domain: private, invitation-only, combined voice/text, E2EE, immutable security mode. This fixture is not a general room-creation API or a ten-user production limit. Text navigation never implicitly joins voice.

Every API response includes JSON; errors contain error and a safe message. Unknown routes/actions fail, malformed input fails before mutation, and protected endpoints authenticate current session/device and authorise the action. GET /api/info declares protocol, room, media endpoint and limits. Unsupported profiles cannot silently downgrade. HTTP status codes: 400 malformed, 401 authentication, 403 forbidden, 404 absent/inaccessible, 409 conflict/stale revision, 410 expired command, 413 too large, 429 rate limit, 503 unavailable. Responses disable caching.

Authentication and admission

Bootstrap requires an operator-generated random secret and creates the sole initial owner, their chosen password and device in one commit. A completed bootstrap cannot run again. No fixed/default credentials exist.

Account invitations bind a lowercase username and purpose and are single-use, expiring and administrator-issued. Under D-052, administrators grant lobby access to existing accounts and those accounts join without another code. Legacy room invitations separately bind an existing account and remain redeemable. Creating an account never implicitly joins it to the room; bootstrap admits the initial owner as part of installation. Only the owner can assign moderator status or reset an account. The owner cannot demote or remove itself through these APIs. SPEC-008 owns the administrator hierarchy.

A device has independent X25519 encryption and Ed25519 signing key pairs. Registration proves possession of its signing key. Login requires both the password and a one-use server challenge signed by the already registered device; knowing a device ID does not establish possession. Another device receives an explicit device-mismatch error. Logout revokes that login session; the registered device identity remains the account's one permitted device until explicit replacement.

Owner-initiated replacement revokes the target's sessions, clears its live attendance and creates a purpose-bound replacement invitation. Redemption sets a new password and new device and increments room revision. The UI explains lost old-history access before replacement. Peers must explicitly trust the changed fingerprint; replacement cannot inherit their prior trust automatically. The owner cannot reset itself through a credentialless recovery endpoint. Lost owner keys require an operator recovery procedure, which V1 does not implement.

Signed login successor (D-103)

login-sessions/1 supersedes this profile's opaque login issuance for new local accounts. SPEC-003 owns claims, defaults, algorithms, lifetime and handover rules. Existing clients may carry the new bearer string as opaque data; only upgraded clients expose verification, metadata and renewal UI. Legacy/visitor credentials remain valid under their existing finite deadlines.

  • GET /api/info: advertises login-sessions/1; limits.sessionMs is the current local policy in milliseconds or null. It does not change existing tokens or media/foreign deadlines.
  • GET /api/login-keys: public, uncached {issuer, audience, keys}; keys is an array of public OKP/Ed25519 JWKs with UUID kid, alg: Ed25519, use: sig. No private d, token-carried key or discovery trust override.
  • Local bootstrap/register/login/link/recovery session responses add session: {account, device, issuedAt, expiresAt, serverNow, signed} alongside {token,self}. Times here are epoch milliseconds; signed JWT times are integer seconds. expiresAt: null means Unlimited.
  • GET /api/session: authenticated home-account metadata with the same shape; optional pending/retiring flags describe handover state. Retiring metadata reports the effective earlier cutoff. Legacy issuance time is inferred from its original fixed lifetime.
  • POST /api/session/renew: current home bearer plus {username,password,deviceId,challenge,signature} using a fresh /api/challenge and existing proof serialization with purpose session-renew. Returns {token,self,session} with pending:true. A prepared token cannot use ordinary endpoints.
  • POST /api/session/activate: prepared bearer plus {signature} over proof("session-activate", token, username, device). Return session metadata. Current approved-device authority and original session are rechecked. Successful retries are idempotent; expired/revoked pending/original authority fails.
  • GET /api/server/login-keys: owner-only {issuer,audience,keys,revision,active}. POST accepts {revision,action:"rotate"} or {revision,action:"revoke",kid} for a non-active trusted key; stale revisions return 409. Rotation must precede active-key revocation. Return updated public keys/revision/active only.
  • /api/server/settings adds loginSessionDays per SPEC-003. /api/logout revokes all old/new/pending credentials for this approved device. Other devices are unaffected.

Session failures return 401 and safe re-sign-in guidance. A token whose login signature/profile fails never falls back to another token protocol. Pending activation has its own explicit admission path; it cannot pass ordinary session authorization. Media tokens remain purpose-bound HMAC grants, distinct from signed account login tokens.

Commands and events

POST /api/tickets issues a short-lived HMAC-authenticated command ticket bound to account, device, room scope, UUID and expiry. A mutation supplies {ticket, action, expectedRevision?, payload}. Canonical JSON recursively sorts object keys, preserves array order, rejects unsupported/non-finite values and has no whitespace. The service binds the first accepted command to the SHA-256 fingerprint of action/revision/payload. Different payload with the same ticket conflicts. Identical retries return the original permitted result; a missing expired result never causes another mutation. Current permission is checked before result disclosure.

Commands include account invitation, room invitation/admission, leave/remove, role assignment, device reset, moderator mute, shared gain and encrypted text send. Settings/membership/moderation commands require the observed room revision; text send requires the exact recipient revision. Admission/removal and text events share monotonically increasing room positions. Persistent membership intervals use inclusive admission and exclusive removal boundaries.

Messages are signed canonical envelopes containing protocol, message UUID, room, sender account/device, room revision and a list of recipient device ciphertexts. The authenticated signer must match the session. Exactly the currently admitted device set must be represented; stale recipients cause conflict. The server stores opaque ciphertext and public authentication metadata, never content keys. The encrypted inner payload binds message ID, room, revision, sender and recipient, preventing context substitution. Message UUID is unique per sender; altered-payload reuse conflicts even with a new command ticket.

History pagination is by host position, at most 50 records per response. Retrieval requires current membership. The service filters every page to the union of that account's membership intervals and the retention window. Old eligible messages encrypted to a lost device remain visibly undecryptable. Reconnect does not create a new membership interval. The client does not queue offline sends; it retains an ambiguous command for retry rather than inventing another command ID.

The V1 event channel is bounded polling of /api/state and /api/history, with the server revision as a cursor. Revisions are hints for refresh, not authorisation. This avoids requiring a persistent worker process. Switching to a future WebSocket channel cannot change the accepted ordering or history contract. D-221 adds SPEC-027 WebSocket hints as an optional accelerator only: polling remains in every state, hints carry no content and authorize nothing, and Durable Object hibernation means sockets still need no persistent worker process.

Media relay contract

Legacy PCM contract, superseded for new calls by D-084 below. The lease, routing, authentication and authority rules remain active.

The separate relay uses WebRTC unordered data channels with zero retransmissions. It forwards application-encrypted mono PCM frames without decoding them. V1's PCM profile trades bandwidth for a small, testable media path; Opus is a future negotiated profile, not an undisclosed dependency. Both managed deployment of this relay and independent self-hosting use the same protocol; no third-party managed-provider compatibility is claimed.

Control issues HMAC-SHA-256 media tokens containing purpose, audience, room, account, device, voice-session ID, policy revision, publication permission, issued time and expiry. The relay rejects wrong purpose/audience/signature, excessive lifetime, future issuance, expired grants and stale revisions. A live peer must renew its lease; every forwarded packet checks both sender and recipient leases. A valid setup token never grants indefinite forwarding.

POST /offer accepts a lease and SDP offer and returns a complete SDP answer. POST /lease refreshes the matching connection's authority. POST /revoke accepts a separate signed control command and advances the room's minimum policy revision. The relay immediately rejects older connections/tokens; newer grants reflect the changed policy. Revocation is idempotent and monotonic. There is no client-side remote-unmute command.

The client creates a data channel named voice-chat/1, unordered with maxRetransmits: 0. A packet has source/target device, voice-session ID, revision, sequence, nonce and ciphertext. The relay derives the sender identity from its admitted connection, verifies ownership fields and sizes, then forwards only to the selected currently authorised recipient. Receivers validate encrypted context and use a bounded replay window. Frames from earlier voice sessions/revisions are rejected. Publication stops across recipient/permission changes until the user makes a fresh PTT press.

Only live admitted devices receive per-recipient voice ciphertext. A persistent member outside voice receives no voice ciphertext or live key material. Text recipient membership is independent. Loss of control closes publication locally and forwarding expires at the relay. The service records moderator state as pending/effective/failed/superseded with its accepted revision and durable external work. Effective requires relay acknowledgement or the demonstrated lease deadline; failure does not clear the deny policy.

Opus wire profile (D-084)

Updated hosts advertise voiceMedia: {profile: "voice-opus/1"} in room/lease state and require voice-opus/1 in x-sloppy-client for voice preparation, join and lease. Missing capability returns 409 upgrade_required. Updated clients reject missing/different state capability before joining and use data-channel label voice-opus/1, unordered with zero retransmissions. Updated relays accept only that label. All parties must update; existing calls must rejoin during cutover. Text/account compatibility is unchanged. No automatic legacy codec or encryption downgrade is allowed.

The audio payload is a binary envelope: byte 0 version 1; byte 1 kind (0 audio, 1 end); bytes 2–5 unsigned big-endian press ID; bytes 6–9 unsigned big-endian frame index. Press IDs start at 1 and increase within attendance; frame indices start at 0 per press. Audio carries one Opus packet after the ten-byte header, bounded by SPEC-004. End carries no audio and its index is the next frame that would have been emitted. Exhaustion fails closed rather than wrapping. Sequence remains monotonically increasing across audio/end packets and recipient replay validation precedes playout. An older press can never replace a newer press, even if it arrives later.

For E2EE, encrypt the entire envelope with SPEC-016's existing AEAD and context. Codec/talkspurt bytes are authenticated as ciphertext; the data-channel/capability boundary selects the format. The relay retains the existing opaque encrypted packet fields. For server-readable rooms, use the existing signed context with profile: "voice-opus/1" and audio: base64url(envelope) replacing pcm. Sign all fields. The relay validates envelope shape/version, bounds, source ownership and signature before forwarding; the receiver validates duration/channel/decoding in the bounded worklet after authentication. Cross-mode/legacy fields fail closed.

All existing packet, quota, lease, revocation, session and room bounds remain active. No new server configuration, persistence or permission is introduced. Tests cover rejected old labels/capabilities, malformed envelopes, mismatched signatures/context, replayed end markers, wrong rooms/revisions and public/encrypted isolation.

Cloudflare preparation extension (D-043)

/api/info adds media: {adapter: "direct"|"cloudflare-turn", enabled: boolean}. All new routes are authenticated same-origin JSON POSTs. POST /api/voice/prepare accepts exactly {id: UUID}. Direct hosting returns {mode:"direct", iceServers:[]}. Cloudflare returns {mode:"cloudflare-turn", id, status:"preparing", expiresAt, retryAfterMs:1000} or a ready result adding {status:"ready", relay, generation:UUID, generationUntil, iceServers}. Times are Unix milliseconds. Each ICE server contains only urls, username, credential; clients receive the fixed UDP and TLS Cloudflare URLs. Ready results are private and uncached. Missing/stale/cancelled attempts use 409 or 410; disabled/provider failure uses 503; rate limits use 429. There is no login-session token in a media request.

POST /api/voice/cancel accepts {id:UUID} and idempotently removes only the matching preparation. Cloudflare POST /api/voice/join requires {preparationId:UUID}; direct mode retains {}. A live lease response adds generation and generationUntil for Cloudflare; its signed media token includes generation with the existing fields/lifetime. Gateway and native relay reject missing/mismatched generations in that mode, even if the signature is valid. Revocation stays separately purpose-bound and cannot start a Container. A private Worker service entrypoint provides prepare/valid/shutdown operations; those operations have no public HTTP routes. Exact generation bounds and rate limits belong to SPEC-002. The private prepare/valid RPC responses use {ok:true,value} or {ok:false,error:{status,code,message}}; unexpected failures are redacted to a generic 503. The control adapter reconstructs the public error contract after RPC serialization.

Consumed, cancelled and expired preparation IDs cannot be reused for one hour. Cancellation also removes a live voice admission created from that same preparation if cancellation raced its join response; it cannot remove a later unrelated session. A fresh attempt uses a fresh UUID and remains subject to preparation limits.

Lobby access extension (D-052)

POST /api/commands adds room.access with {target: accountUUID, allowed: boolean}. It requires the observed room revision and current administrator authority. room.join accepts {} when the caller has access; legacy {token: roomInvitationKey} remains accepted and establishes continuing access. No command grants anonymous admission or bypasses capacity, disabled-account or history rules. Denied access returns 403, malformed input 400 and stale revision 409. Existing ticket idempotency applies.

GET /api/state adds boolean canJoin; the administrator-only /api/accounts adds boolean roomAccess per account. Granting access does not admit the target. Joining opens a membership interval. Voluntary room.leave keeps access for rejoining. room.access with allowed:false and administrative room.remove revoke access, close any current interval, remove live voice and invalidate every outstanding room invitation for the target in one transaction. Already admitted legacy accounts retain access; legacy nonmembers require a grant or valid invitation. Explicit false overrides that compatibility fallback.

Initial owner creation is presented only on /setup (also accepting /setup/); GET/HEAD returns 404 after an owner exists. The bootstrap API continues to reject repeated setup transactionally. Ordinary / always presents user sign-in when signed out. Invitation normalization is a client responsibility: the wire proof and registration API continue to receive the raw key.

Configuration

D-084 Opus framing, version and capability are fixed protocol rules without configurable defaults or persistence; codec/playout values are owned by SPEC-004.

D-055 requests an extension beyond the fixed lobby. The room creation review requires explicit feature discovery, room-bound requests/tickets/content, a transactional storage migration and legacy lobby compatibility before activation. The review is not an implemented wire extension. The current singleton wire contract remains active until that extension is specified, implemented and validated; owner-only creation is confirmed under Q-48 and release presets await Q-47.

This protocol has no independent runtime settings. Numeric defaults and authorised overrides are owned by SPEC-002 through SPEC-008. Protocol version, UUID identities, immutable V1 room mode, one registered active device, mandatory endpoint encryption and distinct credential purposes are fixed profile rules. Unknown configuration is rejected by the owning component; secrets have no defaults.

Testing

The D-103 successor adds exact header/claim/size/duplicate-field and endpoint-binding tests, including legacy compatibility and separation from media grants. See SPEC-003, SPEC-008 and A-42 for current-authority and handover validation.

D-084 wire tests cover envelope shape/size, counters, duration/channel checks, signed field substitution, old labels/capabilities and end-marker replay; use the actual codec for client decode tests.

D-064 profile-command tests exercise exact payload shape, authenticated self-only scope, independent revision comparison, replay/conflict and additive legacy identity compatibility.

For D-052, test typed access payloads, additive state fields, legacy room-token compatibility, code-free admission subject to permission, stale revisions and changed-payload retries. See A-38 and component tests in SPEC-005/008; persistence/history tests belong to SPEC-006.

Protocol units cover canonical encoding; invalid shapes and bounds; signature/purpose/audience/expiry; changed-payload retries; stale revisions; exact recipient sets; duplicate message IDs; interval history; login/device proof; replay windows; token expiry on already established forwarding; and permission checks independent of the UI. Integration tests use real SQLite, signed credentials, endpoint encryption and real relay connections. Native PTT and actual audio capture/playout require separate platform tests.

Acceptance criteria

  • D-103 wire fixtures satisfy A-42 without changing media, visitor, command, device-link or recovery token lifetimes.
  • D-084: incompatible clients and malformed/mixed-mode packets fail closed; the new capability/label and authenticated Opus envelopes work for both room encryption modes without a fallback.
  • An account-profile/1 client can save/clear its own nickname with replay-safe commands without invalidating voice or modifying another identity.
  • A-38 passes for the D-052 workflow without changing existing owner credentials or sessions.
  • Run the local control service, relay and shared client from documented commands; build the Workers bundle and desktop artefacts without secrets embedded in client assets.
  • Create the owner once, register from account invitations, grant lobby access and join without another code; retain legacy room-key redemption and reject reused/expired/wrong-purpose keys. Reject login without the registered device’s proof. See A-32/A-38.
  • Exchange authenticated ciphertext and reconnect to eligible history; tampered messages, stale recipient sets, pre-admission and absence-period history fail. See A-15/A-16/A-27.
  • A removed, muted or expired publisher cannot forward through an already established channel beyond the approved hard bound, including loss of the control service. See A-10/A-29.
  • Independent tests distinguish local mute/gain from room-wide policy and preserve a fresh-press requirement after any interruption. See A-07–A-11/A-24.
  • Report actual tested platforms and human cohort separately from build/test results; do not label this profile's implementation as verified V1 until SPEC-007's applicable evidence exists.

Account profile extension (D-064)

GET /api/info advertises account-profile/1. Public identity projections add optional nickname (default "") and profileRevision (default zero) under SPEC-008. Unknown additive identity fields are ignored by older clients; device proofs, signed roster chains and existing identity IDs remain unchanged. Nicknames are service-authoritative presentation metadata, not signed identity evidence.

The existing authenticated, device-bound ticket/command route adds account.profile with exactly {nickname: string, profileRevision: nonnegativeSafeInteger}. The operation always targets the caller; no target/role/device fields are accepted. Unlike room-policy commands, its room expectedRevision is not its concurrency guard: compare account profileRevision instead, independent of room membership. Return {self: Identity} on success. Use existing command deduplication/audit and rate limits; retrying an accepted ticket returns its original result even after another update. Invalid fields return 400, stale profile revision returns 409 profile_revision, and session failures retain existing 401/403 behavior. Stored legacy account records need no destructive schema migration. No media grant includes or depends on nickname.