spectacles-cloud
Spectacles Cloud — Reference Guide
Snap Cloud is Snap's managed backend platform, powered by Supabase. It gives Spectacles lenses a full backend out of the box: relational database, real-time subscriptions, file storage, and serverless functions.
Official docs: Spectacles Home · Snap Cloud · Internet Access (required for fetch)
Architecture Overview
Lens (Spectacles)
│
├─── Snap Cloud REST API ──► Supabase Postgres DB
│ ├─ Realtime subscriptions
│ ├─ Storage buckets
│ └─ Edge Functions (serverless)
│
Companion Web App ──────────► Same Supabase project
Setup
- Go to cloud.snap.com and create a project.
- Note your Project URL and anon/public API key.
- Enable Internet Access in Lens Studio: Project Settings → Capabilities → Internet Access.
Without this, all
fetch()calls will silently fail on-device. - Store the URL and key as constants in your script.
⚠️ Security — anon key is not secret: The anon key is compiled into the lens and can be extracted by anyone who decompiles it. Anyone who obtains the key can read and write data while RLS is disabled. Always enable RLS (see below). For production lenses, proxy all database access through a Snap Cloud Edge Function so the key never leaves the server.
Database (Postgres via REST)
const PROJECT_URL = 'https://<project-ref>.supabase.co'
const API_KEY = 'your-anon-key'
// Read rows
async function getMessages(): Promise<any[]> {
const r = await fetch(`${PROJECT_URL}/rest/v1/messages?order=created_at.desc&limit=20`, {
headers: {
'apikey': API_KEY,
'Authorization': `Bearer ${API_KEY}`
}
})
if (!r.ok) { print('Read error: ' + r.status); return [] }
return r.json()
}
// Insert a row
async function addMessage(content: string): Promise<void> {
await fetch(`${PROJECT_URL}/rest/v1/messages`, {
method: 'POST',
headers: {
'apikey': API_KEY,
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'Prefer': 'return=minimal'
},
body: JSON.stringify({ content, author: 'spectacles-user' })
})
}
// Update a specific row
async function updateMessage(rowId: string, content: string): Promise<void> {
await fetch(`${PROJECT_URL}/rest/v1/messages?id=eq.${rowId}`, {
method: 'PATCH',
headers: {
'apikey': API_KEY,
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
})
}
Realtime Subscriptions
Supabase Realtime uses WebSockets. Lens Studio's network stack supports WebSockets on Spectacles.
const ws = new WebSocket(
`wss://${PROJECT_REF}.supabase.co/realtime/v1/websocket?apikey=${API_KEY}&vsn=1.0.0`
)
ws.onopen = () => {
// Subscribe to a specific table using the correct postgres_changes format
ws.send(JSON.stringify({
topic: 'realtime:public:messages',
event: 'phx_join',
payload: {
config: {
postgres_changes: [
{ event: '*', schema: 'public', table: 'messages' }
]
}
},
ref: '1'
}))
}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data as string)
if (msg.event === 'postgres_changes') {
const newRow = msg.payload.data.record
displayInScene(newRow)
}
}
ws.onerror = (error) => print('WebSocket error: ' + JSON.stringify(error))
Reconnect on Spectacles sleep
Spectacles can disconnect WebSockets when the device sleeps. Add a reconnect loop:
let ws: WebSocket | null = null
function connect(): void {
ws = new WebSocket(`wss://${PROJECT_REF}.supabase.co/realtime/v1/websocket?apikey=${API_KEY}&vsn=1.0.0`)
ws.onopen = () => subscribeToTable(ws!)
ws.onclose = () => scheduleReconnect()
ws.onerror = () => scheduleReconnect()
}
function scheduleReconnect(): void {
if (!retryCount) retryCount = 0
if (retryCount++ >= 5) { print('[Cloud] WebSocket gave up after 5 retries'); return }
const retry = this.createEvent('DelayedCallbackEvent')
retry.bind(() => connect())
retry.reset(3) // retry after 3 seconds
}
let retryCount = 0
Cloud Storage (Files / Images)
async function uploadImage(bucketName: string, path: string, base64Data: string): Promise<void> {
const binary = Base64.decode(base64Data) // convert to ArrayBuffer
await fetch(`${PROJECT_URL}/storage/v1/object/${bucketName}/${path}`, {
method: 'POST',
headers: {
'apikey': API_KEY,
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'image/jpeg'
},
body: binary
})
}
function getPublicUrl(bucketName: string, path: string): string {
return `${PROJECT_URL}/storage/v1/object/public/${bucketName}/${path}`
}
Serverless Edge Functions
Edge Functions are Deno TypeScript functions deployed to Snap Cloud. Use them for server-side logic, aggregations, or keeping API keys off the device.
async function callFunction(fnName: string, payload: object): Promise<any> {
const r = await fetch(`${PROJECT_URL}/functions/v1/${fnName}`, {
method: 'POST',
headers: {
'apikey': API_KEY,
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
return r.json()
}
Recommended production pattern: Edge Function as auth proxy
For any lens you share publicly, route database access through an Edge Function instead of calling the REST API directly from the lens:
Lens → Edge Function (holds service-role key) → Supabase DB
The Edge Function can validate inputs, enforce business rules, and return only the data the lens needs — the raw DB key never leaves the server. The lens only needs the anon key to call the Edge Function, and with RLS that key has no direct table access.
Lens-side call (TypeScript)
// The lens calls the Edge Function — it never touches the DB directly
async function addMessageSecure(content: string): Promise<void> {
const r = await fetch(`${PROJECT_URL}/functions/v1/add-message`, {
method: 'POST',
headers: {
'apikey': API_KEY, // anon key only — Edge Function validates and writes
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
})
if (!r.ok) print('Error: ' + r.status)
}
Edge Function (Deno TypeScript, runs on Snap Cloud)
// supabase/functions/add-message/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js'
Deno.serve(async (req) => {
const { content } = await req.json()
// Validate inputs server-side before writing
if (!content || typeof content !== 'string' || content.length > 500) {
return new Response('Invalid input', { status: 400 })
}
// Use the service-role key here — it never leaves the server
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // NOT the anon key
)
const { error } = await supabase
.from('messages')
.insert({ content, author: 'spectacles-user' })
if (error) return new Response(error.message, { status: 500 })
return new Response('OK', { status: 200 })
})
Row-Level Security (RLS)
Supabase supports RLS policies to restrict who can read/write which rows. Always enable RLS on every table before sharing a lens.
-- Minimal public read (ok for leaderboards / shared content)
CREATE POLICY "public_read" ON messages
FOR SELECT USING (true);
-- User-scoped write (requires Snap auth JWT)
CREATE POLICY "owner_insert" ON messages
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Avoid USING (true) on INSERT/UPDATE/DELETE — this lets any key holder modify all data
⚠️
USING (true)on writes is not protection. It means any holder of the anon key can mutate every row. Use it only for genuinely public read-only data, and never on write operations in a shared lens.
For user-specific data, pass a JWT from the lens (requires Snap's auth integration), or use the Edge Function proxy pattern.
Permissions & Privacy
Combining internet access with camera, microphone, or location triggers Snap's Transparent Permission system: when the lens launches, the OS shows the user a dialog listing which sensitive data the lens accesses, and the device LED blinks while capture is active. Plan your UX around this prompt — it will appear before the lens starts.
Common Gotchas
- Enable Internet Access capability first — Project Settings → Capabilities → Internet Access. Without it, fetch silently fails on-device.
- The anon key is embedded in the lens. Anyone can extract it from a published lens. Always enable RLS and consider the Edge Function proxy pattern for production.
- The Supabase Realtime WebSocket URL requires the anon key (
?apikey=...) — this is a Supabase protocol requirement, not something you can avoid. Mitigate exposure with RLS: even if someone extracts the key, RLS limits what they can read or write. - WebSocket
postgres_changesformat: the payload must include{ config: { postgres_changes: [...] } }in thephx_joinmessage — a simpler topic-only join will not receive row change events. - WebSocket connections drop when Spectacles goes to sleep — implement a reconnect loop with
DelayedCallbackEvent. - Large payloads in Realtime subscriptions can slow the lens — subscribe only to the columns you need.
- Supabase free tier has connection and storage limits — upgrade the plan or add connection pooling for production.
- CORS: If building a companion web app, add your web app's domain to the Supabase CORS allow-list.
More from rolandsmeenk/lensstudioagents
lens-studio-scripting
Reference guide for the Lens Studio TypeScript component system — covering the @component, @input, @hint, @allowUndefined, and @label decorators, the BaseScriptComponent lifecycle (onAwake vs OnStartEvent, UpdateEvent, DelayedCallbackEvent one-shot and repeating timers, TurnOnEvent/TurnOffEvent, onDestroy), accessing components with getComponent (plus null-check patterns to fix 'cannot read property of null' errors), cross-TypeScript imports with getTypeName(), NativeLogger vs print, prefab instantiation (sync and async), SceneObject hierarchy queries, and enabling/disabling objects. Use this skill whenever writing or debugging any Lens Studio TypeScript script, wiring up scene objects, or fixing 'this is undefined' or null-reference errors — platform-agnostic (works for Spectacles and phone lenses).
12spectacles-lens-essentials
Reference guide for foundational Lens Studio patterns on Spectacles — covering the GestureModule (pinch down/up/strength, targeting, grab, phone-in-hand with correct TypeScript API), SIK components (PinchButton, DragInteractable, GrabInteractable, ScrollView), hand-tracking gestures, physics bodies/colliders/callbacks (including audio-on-collision), LSTween animation (position/scale/rotation/color tweens), prefab instantiation at runtime, materials (clone-before-modify), spatial anchors, on-device persistent storage (putString/getFloat), spatial images, and the Path Pioneer raycasting pattern. Use this skill for any Spectacles lens that needs interaction, motion, animation, physics, audio, or persistent local storage — including Essentials, Throw Lab, Spatial Persistence, Spatial Image Gallery, Path Pioneer, Public Speaker, Voice Playback, Material Library, and DJ Specs samples.
9lens-studio-world-query
Reference guide for world understanding and scoring in Lens Studio — covering WorldQueryModule HitTestSession (HitTestSessionOptions.filter for jitter smoothing, semantic surface classification for floor/wall/ceiling/table detection, null result handling, per-frame performance), SIK InteractionManager targeting interactor ray pattern, Physics.createGlobalProbe().rayCast for scene-collider hits with collision layer filtering, aligning objects to surface normals using quat.lookAt, and the LeaderboardModule (create/retrieve with TTL and OrderingType, submitScore, getLeaderboardInfo with UsersType.Global/Friends). Use this skill when detecting real floors/walls/tables to place AR content, raycasting for hover or interaction against scene objects, or adding a global in-lens leaderboard — differentiates from spectacles-lens-essentials (physics/SIK) and from spectacles-cloud (Supabase persistence).
8lens-studio-materials-shaders
Reference guide for materials and shaders in Lens Studio — covering runtime material property changes (clone-before-modify, mainPass.baseColor, mainPass.opacity, mainPass.baseTex), blend modes (Normal/Alpha/Add/Screen/Multiply), depth and cull settings (depthTest, depthWrite, twoSided, cullMode), render order, material variants, assigning textures and render targets, reading and writing RenderTarget textures for post-processing, the graph-based Material Editor node system, custom shader graph nodes, and common shader pitfalls. Use this skill for any lens that needs to change material colors or textures at runtime, implement custom visual effects with shaders, set up post-processing render pipelines, chain render targets, or debug material/blend-mode issues — covering MaterialEditor, Drawing, and HairSimulation examples.
6spectacles-networking
Reference guide for the Lens Studio Fetch API and WebView component in Spectacles lenses — covering InternetModule (Lens Studio 5.9+), Fetch API via internetModule.fetch(Request) with bytes/text/json response handling, performHttpRequest, Internet Access capability, GET/POST requests, custom headers, Bearer auth, polling, timeouts, CORS/HTTPS, WebSocket and RemoteMediaModule for media from URLs, and bidirectional WebView messaging. Use this skill for any lens that calls a REST API, polls a JSON endpoint, loads remote images, embeds a webpage, or talks to a custom backend — including the Fetch sample. Use spectacles-ai for LLM/RSG calls, or spectacles-cloud for Supabase/Snap Cloud integration.
6spectacles-connected-lenses
Reference guide for real-time multiplayer AR on Spectacles using Connected Lenses and Spectacles Sync Kit — covering session creation/joining with joinOrCreateSession (including 'already-in-session' error handling), TransformSyncComponent for position/rotation replication, RealtimeStore for shared key-value state (max 512 bytes per key), NetworkEventSystem for one-shot broadcast events, EntityOwnership for physics authority, Lens Cloud for persistent cross-session data, and patterns for turn-based (Tic Tac Toe) and real-time physics (Air Hockey). Also covers late-joiner state sync, transform drift mitigation, and store size limits. Use this skill whenever multiple Spectacles users need to share AR objects or state — covering Tic Tac Toe, Air Hockey, Laser Pointer, High Five, Shared Sync Controls, Spectacles Sync Kit, and Think Out Loud samples.
5