Architecture

Skein has four deployable parts and one Unity package:

 Unity Editor (package)                         Browser
 ┌──────────────────────────┐                  ┌───────────────┐
 │ Skein window / Scene view│                  │ Next.js site  │
 │ SceneTracker → mutations │                  │ + dashboard   │
 │ CollabClient (protocol)  │                  └──────┬────────┘
 └───────┬───────────┬──────┘                         │ same-origin /api/* (rewrite)
   HTTPS │           │ WSS (skein.v1)                 │
         ▼           ▼                                ▼
 ┌──────────────┐  ┌───────────────────────┐   ┌──────────────┐
 │  API         │  │  Realtime nodes (N)   │   │  web (Next)  │
 │  Fastify     │  │  ws + Lua-in-Redis    │   └──────────────┘
 │  REST + auth │  │  presence/locks/sync  │
 └──┬───────┬───┘  └──┬──────────────┬─────┘
    │       │ ctl:realtime pub/sub  │
    │       └────────────► Redis ◄──┘  session state, ordering, pub/sub, replay log, rate limits
    │                         │
    ▼                         ▼ (persister: Redis stream → Postgres)
 PostgreSQL  ◄────────────────┘  users, tenants, projects, sessions, changes, snapshots, billing, audit
 Object storage (S3/R2/MinIO or local disk) — snapshot scene files
Component Path Responsibility
Protocol packages/protocol Versioned wire contract: message schemas (zod), limits, close codes, error messages. Shared by API, realtime and tests; mirrored in C#.
Shared packages/shared Plans/limits, roles/permission matrix, participant colors, Redis key layout, control-plane message types.
API apps/api Accounts, auth (web sessions, device flow, OAuth), tenancy, projects, members, sessions + realtime tickets, history, snapshots, API keys, billing (Stripe), audit logs, OpenAPI.
Realtime apps/realtime WebSocket server. Authenticates tickets, relays presence, grants locks, sequences mutations, replays on reconnect, persists changes. Stateless per node — all session state lives in Redis.
Web apps/web Marketing site, docs, auth pages, device-approval page, dashboard. Talks to the API through a same-origin /api rewrite.
Unity package unity/Packages/com.skeinlabs.collab Runtime/Core (engine-agnostic: JSON, protocol, transport, client state machine, presence math, REST client) and Editor (identity, change tracking, applying remote changes, Scene view drawing, UI).

Technology decisions

Decision Choice Why
Language (server) TypeScript (strict) on Node 22 One language for API, realtime, web and the protocol definitions; zod schemas give runtime validation and static types from one source.
HTTP framework Fastify 5 Fast, small, first-class hooks for auth/rate limiting, good OpenAPI support. NestJS's DI container was unnecessary at this size — dependencies are passed explicitly through an AppContext.
Realtime transport Raw WebSockets (ws), JSON frames, sub-protocol skein.v1 Works through every corporate proxy and with .NET's ClientWebSocket in Unity without native plugins. WebRTC adds NAT traversal and a signaling tier for no benefit in a server-authoritative design.
Encoding JSON with quantized numbers Debuggable, schema-validated, ~330 bytes per camera frame. A binary codec can be negotiated later via the sub-protocol (skein.v2) without changing semantics.
Session state Redis (Lua scripts, pub/sub, streams) Every mutation is one atomic script: dedup → lock check → INCR seq → state update → replay-log append → publish. That single step is what gives a global, gap-free order across any number of realtime nodes.
Durable store PostgreSQL 16 Relational tenancy model, foreign keys, transactional plan-limit checks, JSONB for change payloads.
Persistence of changes Redis stream + consumer group → Postgres Keeps Postgres off the hot path; consumer groups survive node loss; inserts are idempotent on (session_id, seq).
Unity UI UI Toolkit (window) + IMGUI/Handles (Scene view) UI Toolkit is the modern editor UI; Handles is the right tool for drawing in the Scene view.
Unity identity GlobalObjectId + session-assigned ids Stable across machines for committed scenes, and requires no components added to user scenes.
Website Next.js 16 (App Router) + Tailwind 4 Static marketing/docs pages, server-side auth gate for the dashboard, one deployable.
Passwords argon2id (OWASP parameters) Memory-hard; @node-rs/argon2 ships prebuilt binaries (no compiler needed in containers).

The hardest technical risks (and how they're handled)

  1. Ordering and exactly-once delivery across multiple realtime nodes. Solved by doing all ordering inside Redis Lua scripts (atomic), publishing in the same script, assigning a per-session seq, and de-duplicating client event ids for 5 minutes. Clients detect gaps and request replays. Tested with racing clients and two nodes (apps/realtime/test).
  2. Stable object identity in Unity without polluting scenes. GlobalObjectId is derived from the scene GUID + local file id, so it matches on every machine with the same committed scene. Objects created during a session get server-shared n: ids. Requirement: collaborators open the same committed scene (documented).
  3. Detecting local edits without polling or GC churn. ObjectChangeEvents + Undo.postprocessModifications mark touched objects; each is diffed against a cached baseline once the gesture ends. Drags stream previews at ≤20 Hz.
  4. Conflicts. There is no meaningful merge of two transforms. Skein uses server-granted leases (locks) and reverts edits to objects someone else owns. See conflicts.md.
  5. Unity domain reloads. Every script compile destroys all managed state including sockets. The client detaches without leaving, persists a resume token in SessionState, and resumes with the same connection id — keeping its locks.
  6. Not trusting the client. Every frame is schema-validated; identity comes from the signed ticket, never from the message; permissions are rechecked server-side (viewer = read-only, lock ownership, admin-only force unlock).

Data flow of an edit

  1. User drags Door_Main (lock already held — it was requested on selection).
  2. While dragging: OBJECT_TRANSFORM_PREVIEW at ≤20 Hz → realtime node checks the lock in its bus-fed lock cache → publishes to the session channel → other clients apply it without touching Undo.
  3. Mouse up: OBJECT_TRANSFORM with prev and a label. The node runs COMMIT_MUTATION in Redis: dedup, verify lock owner + lease, seq = INCR, update object state hash, append to replay stream (<seq>-0), append to the global change stream, publish relay.
  4. Sender receives ACK {seq}; everyone (sender included) receives the sequenced relay in order.
  5. A persister (any node, consumer group) inserts the change into changes with before/after.

Database schema

Defined in apps/api/migrations/0001_init.sql. Key tables:

  • users, oauth_accounts, auth_sessions (browser), refresh_tokens (editor, rotating families), device_codes, email_tokens
  • organizations, organization_members — tenants; every user gets a personal organization on sign-up
  • projects, project_members, invitations
  • collab_sessions, session_participants
  • changes (unique (session_id, seq)), snapshots
  • subscriptions, stripe_events (webhook idempotency), usage_counters
  • api_keys, audit_logs

Every tenant-owned row carries org_id/project_id; all project access goes through one function (requireProjectAccess) that resolves the effective role with a single join and returns 404 for both "doesn't exist" and "no access".

Repository layout

apps/api            REST API (Fastify)          apps/api/migrations   SQL migrations
apps/realtime       WebSocket server            apps/web              Next.js site + dashboard
packages/protocol   wire protocol (TS)          packages/shared       plans, roles, keys
unity/Packages/com.skeinlabs.collab   the Unity package (UPM)
unity/SampleProject                   Unity project using the package via file: reference
unity/Tooling                         .NET projects: compile checks + C# tests
tests/e2e           cross-language end-to-end tests        tests/load   load generator
infrastructure      Dockerfiles, Kubernetes-ready manifests, Caddy config
docs                this documentation (also rendered at /docs on the website)