Spheres
Anonymous real-time 3D social space — pnpm monorepo, React 19 + Three.js + R3F client, dedicated Socket.io world server on Railway, Firebase only for auth and sparse Firestore writes. Positions, presence, and chat never touch the database.
pnpm workspaces · shared types between client and server
spheres/
├── apps/
│ ├── web/ React 19 + Vite + TypeScript
│ │ └── src/
│ │ ├── pages/ LoginPage · AccountPage · WorldPage
│ │ ├── components/ world/ (R3F) · AuraPicker · SpherePreview
│ │ ├── stores/ authStore · userStore · worldStore
│ │ ├── lib/ firebase.ts · firestore.ts · socket.ts
│ │ └── i18n/ en / he / ru
│ └── world-server/ Node.js 22 + Express + Socket.io
│ └── src/
│ ├── index.ts Socket event router · AI tick loop
│ ├── world.ts WorldManager (in-memory Map, max 50 players)
│ ├── auth.ts Firebase Admin token verification
│ ├── ai.ts Generic AI sphere behaviour
│ └── jul/ JUL — persistent AI character (9 files)
└── packages/
└── shared/ PlayerState · Vec3 · AuraType · protocolBrowser · React 19 + Vite + Three.js + R3F + React Router 7
WorldPage renders 3D scene — flight controls, sphere meshes, ChatOverlay, ContactOverlay, RatingOverlay. Zustand drives all UI state.
Firebase · Auth + Firestore (sparse writes)
User profiles · aura · coreValue · rating cooldowns · reports — NOT positions, presence, or chat
World server · Node.js 22 + Express + Socket.io 4
Token verify (Admin SDK) · WorldManager in memory · chat relay · JUL AI tick · 10 Hz broadcast loop
Deployment
Web · Vercel
Static Vite build, CDN-distributed
world-server · Railway
Auto-deploy from GitHub main
OpenRouter
LLM API powering JUL conversations
useAuthStore
Firebase User, loading, error, initialized. Methods: init · signIn · signUp · signInWithGoogle · signOut · sendVerification · reloadUser
useUserStore
Firestore UserProfile: aura · coreValue · language · displayName. Written on registration + debounced aura / language changes.
useWorldStore
Socket, remotePlayers map (prevPos / targetPos), contactState machine, chatMessages, cooldowns, ratingFeedback, kickedMessage.
From login token to smooth remote spheres
Firebase Auth
Login → ID token issued client-side
connect()
Socket.io client, token passed in auth{}
join_world
Server verifies token via Admin SDK
update_state
Client emits pos + rotation at 10 Hz
Broadcast
Server → player_update to world room
Interpolate
prevPos→targetPos smoothed in R3F loop
worldStore.contactState on client · PlayerStatus in WorldManager on server
request_contact. Server validates both players idle, then emits incoming_request to target. Times out after 30 s.respond_contact (accept: true). Server emits contact_started to both players.end_chat. Server checks 24-hour cooldown, emits chat_ended with optional ratingCooldownUntil. rate_core applies a ±0.05 step to target's coreValue in memory + Firestore, then broadcasts core_updated to all world players.Types defined in packages/shared/src/protocol.ts
uid, email, displayName, language, aura, coreValue, createdAt, updatedAt
Written on registration + debounced aura/language changes. coreValue updated after each rating.
raterUid, targetUid, timestamp
key = 'raterUid:targetUid'. 24-hour cooldown. Checked server-side before applying rate_core.
Conversation history and relationship data between JUL and each user
One document per user who has chatted with JUL. Enables persistent memory across sessions.
reporterUid, targetUid, timestamp, worldId
Admin-only read. Created by report_user event handler.
WorldManager (a plain TypeScript Map) and are discarded on disconnect. Expected Firestore usage: fewer than 100 reads/writes per session.Each player's sphere is fully defined by two values — no username, no avatar, no profile photo.
AuraType · 16 moods
enlightened · inspiration · joy · gratitude · confidence · calm · neutral · doubt · anxiety · sadness · apathy · irritation · anger · despair · hopelessness · sos
coreValue · −1 to +1
Starts at 0. Drifts ±0.05 per rating received. Controls sphere colour spectrum — warm hues for positive, cool/dark for negative. A visible emergent reputation with no explicit profile or score display.
JUL (uid: "jul") is a persistent AI character spawned in the default world (global-1). Powered by OpenRouter, running on the world server alongside human players.
personality.tsFixed character traits, tone, and communication style baked into the system prompt.
memory.tsReads/writes jul-memory/{uid} in Firestore. Conversation history persists across sessions.
behavior.tsHigh-level state machine: wandering → approaching → initiating contact.
targeting.tsSelects which idle player to approach based on proximity. Ignores players on cooldown.
movement.tsSmooth navigation toward target. Writes directly to WorldManager — JUL is a real player entry.
conversation.tsActive chat tracking: message locks, daily conversation limits per user.
prompt.tsAssembles LLM prompt from personality + memory + conversation context.
providers/openrouter.tsOpenRouter API integration. JUL's mood maps to AuraType, broadcast via core_updated.
tickJul() which may produce an initiate_contact action. JUL then goes through the full request_contact → contact_started → chat → rate_core flow — exactly like a human player. After chat ends, JUL rates the player and updates its Firestore memory.WORLD_CONFIG: defaultWorldId global-1 · maxPlayersPerWorld 50 · contactRequestTimeoutMs 30 000 · ratingCooldown 24 h