snoozestack-js — JavaScript & TypeScript
snoozestack-js is a dependency-free client for a project's identity, functions, queues, and Signals — plus the defineTable() schema vocabulary the runtime builds your database from. Install it from snoozestack.com/js.
import { createClient } from 'snoozestack-js'; const snoozestack = createClient( 'https://<project-ref>.snoozestack.com/api', // Project Overview SNOOZESTACK_ANON_KEY, // the public project key);Every service hangs off that /api base — /api/auth, /api/functions/v1, /api/storage/v1 and the rest — and a custom domain serves the identical paths under its own name. In a browser app you can pass the base on its own:
// Same-origin: resolves against whichever host serves the page, so the same// build works on <project-ref>.snoozestack.com and on a custom domain.const snoozestack = createClient('/api', SNOOZESTACK_ANON_KEY);A relative base needs a browser to resolve against, so on a server pass the absolute/api URL instead. See Websites for publishing the app itself.
Database#
There's no query builder on this client, deliberately. A table is only reachable from inside a function, through its declared db capability — the client calls the function instead:
const { data: clubs, error } = await snoozestack.functions.invoke('list-clubs'); await snoozestack.functions.invoke('add-club', { body: { name: '7 iron', distance: 165 },});export default async function handler(req: Request, capabilities) { const { rows } = capabilities.db.query( "select id, name, distance from clubs where in_bag = 1 order by distance desc limit 20", ); return Response.json(rows);}The schema itself is also this package's job — defineTable() and schema.* in your committed schema.mjs are imported from snoozestack-js. See Schema & migrations and Permissions.
Application identity#
Projects on the runtime use SnoozestackIdentity instead: a small state machine (anonymous → pending → authenticated → expired) over the project's own /auth endpoints, with sessions verified locally by the runtime. All the sign-in methods share it — magic link, Google, Apple, email/password, and native id_token for apps that never see a browser redirect.
import { SnoozestackIdentity } from 'snoozestack-js'; const identity = new SnoozestackIdentity(PROJECT_URL, ANON_KEY); // Magic link: request, then complete on the emailed link's landing pageawait identity.requestMagicLink({ email, redirectTo: 'https://app.example.com/' });const { user } = await identity.completeMagicLink(); // reads ?token=… itself // Google / Apple: one call each — the provider name is the only difference.// Both navigate the browser to the provider and come back to redirectTo.identity.startOAuth({ provider: 'google', redirectTo: 'https://app.example.com/' });identity.startOAuth({ provider: 'apple', redirectTo: 'https://app.example.com/' });const oauth = await identity.completeOAuthCallback(); // on the redirectTo page // Email/password: sign-up stores the password pending until the email is// proven (the standard verification email goes out) — so nobody can squat// an address they don't own. Sign-in is immediate once a password exists.await identity.signUpWithPassword({ email, password, redirectTo: 'https://app.example.com/' });const signedIn = await identity.signInWithPassword({ email, password }); // A magic-link/OAuth-only user sets a first password by omitting currentPassword.await identity.changePassword({ currentPassword, newPassword }); // Self-serve deletion: remove your own data about the user first, then this// deletes the identity and signs out locally.await identity.deleteAccount(); // Native apps skip the redirect entirely: Google's SDK and Sign in with// Apple hand you a signed id_token, so there is no callback to complete.// nonce is the RAW value you generated, never its SHA-256 — Apple puts the// hash in the token and the server compares the two.await identity.signInWithIdToken({ provider: 'apple', idToken, nonce });await identity.signInWithIdToken({ provider: 'google', idToken }); identity.onStateChange((state, session, user) => { /* re-render */ });await identity.signOut();Sessions refresh themselves before expiry and persist in localStorage, namespaced per project and per namespace. Failures reject with a stable code (invalid_credentials, rate_limited, password_too_weak, …) on SnoozestackIdentityError. Google and Apple need the project's own provider credentials declared in project.toml — see Authentication for the per-provider setup, including the Apple Services ID walkthrough.
Against snoozestack dev there is a built-in account so you can reach a signed-in screen with no provider set up and no email sent: sign in as user with the password 1234. The Google and Apple buttons use it too — locally they skip the provider entirely and come back signed in as that same account, so the callback handling you write is the code that runs in production. The account exists only in the local dev server; nothing hosted — project host, custom domain, or preview — has it.
Storage#
The SDK ships no storage client. Uploads and downloads are mediated by your own functions, which keeps the authorization decision in code you wrote rather than in a policy evaluated by a service the client talks to directly.
// Ask one of your functions to authorize a path — it returns a presigned URL.const { data: signed, error } = await snoozestack.functions.invoke('avatar-upload-url', { body: { contentType: file.type },}); // A presigned URL carries its own signature, so this is a plain PUT.await fetch(signed.url, { method: 'PUT', body: file, headers: { 'content-type': file.type } }); // A public bucket needs no call at all — the object URL is stable:const src = `${SNOOZESTACK_URL}/storage/v1/object/public/avatars/${path}`;See Storage for buckets, sharing files, and access control.
Functions#
// POST by default. The signed-in session is attached automatically.const { data, error } = await snoozestack.functions.invoke('chat', { body: { messages },}); // A reading function is a GET.const { data: wallets } = await snoozestack.functions.invoke('wallets', { method: 'GET' });An error is returned, not thrown — check error before using data. error.message carries the function's own message when it sent one.
Signals#
snoozestack.signals.identify(user.id); // merges their anonymous history insnoozestack.signals.track('signup', { plan: 'pro' }); // batched; never blocks, never throwssnoozestack.signals.reset(); // on sign-outAlso usable standalone as SnoozestackSignals, with no other client needed. See Signals for batching, identity, and the export API.
Queues#
await snoozestack.queues.send('jobs', { kind: 'booking-confirmed', id: 42 });Enqueueing is trusted — see Queues for why a browser never does it directly.
Environment variable conventions#
SNOOZESTACK_URL=https://<project-ref>.snoozestack.com/apiSNOOZESTACK_ANON_KEY=eyJ… # the public project key — safe in browsersIn Vite use the VITE_ prefix (import.meta.env.VITE_SNOOZESTACK_ANON_KEY); in Next.js, NEXT_PUBLIC_ for browser-side values. Realtime subscriptions are not supported.