Authentication
Sign users in with SurrealDB access methods, and scope every live query with row-level permissions.
Authentication in Sp00ky is built directly into the sql schema, making it robust, typesafe, and dynamic. Because access control is defined at the database layer, your rules are consistently enforced regardless of how the data is accessed.
Schema Configuration
You define authentication using DEFINE ACCESS. This allows you to specify exactly how users sign up and sign in, and what permissions they have.
Client-Side Authentication
Sp00ky provides a strongly-typed authentication service on the client. It integrates seamlessly with the schema definitions.
Sign In
Sign Up
Sign Out
Reacting to Auth State
You can subscribe to authentication state changes to update your UI dynamically. The callback receives the user ID when authenticated, or null when signed out.
Local Data Isolation
Every auth change also switches the client’s local storage bucket. Each user has their own IndexedDB store on the device (signed-out sessions use a shared anonymous bucket), so:
- After sign-out or an account switch, the previous user’s cached rows are no longer readable, live queries emit an empty result immediately and refill from the server for the new session.
- Signing out does not delete the user’s bucket: their cache stays warm and any un-pushed offline mutations remain queued in their bucket, resuming automatically the next time they sign in on that device.
- Mutations made while signed out stay in the anonymous bucket; they are not replayed under a signed-in user’s identity.
Nothing to configure. This is the default behavior with
database.store: 'indexeddb'. Details in
Architecture → Per-User Local Buckets.
Row-Level Permissions in Live Queries
A table’s PERMISSIONS FOR select clause is more than a server-side gate: Sp00ky
compiles it into a row filter that is injected into every live query on that
table, enforced identically by the server SSP and the in-browser WASM processor.
A client never registers a query that could return rows its permission forbids,
and the filter is re-evaluated incrementally as data changes.
Supported expressions
- Field comparisons:
=,!=,<,<=,>,>=, and string prefix matches. - Boolean composition with
ANDandOR. - The
$authand$accessparameters (e.g.owner = $auth.id,$access = "account"). - Relational membership via
IN (SELECT VALUE <field> FROM <table> WHERE …). The innerWHEREmay itself reference$auth. This lowers to an incremental semi-join, so when the referenced table changes (a broadcast is made public, a share is revoked) the affected live queries update on their own. No refetch. - A subquery that projects a record-link field (
SELECT VALUE link.field) resolves the link with a nested join, so two-hop rules like “rows whose owner is the owner of a broadcast shared with me” work.
All three branches above are enforced on the client too. A viewer only ever syncs the presence rows they are actually allowed to see; making a broadcast private retracts its rows from every unrelated viewer’s live query.
Not supported
EXISTS (SELECT …) and $parent correlation inside a permission clause are
not representable in the live-query engine and are rejected at registration
time with the offending table named. Rewrite EXISTS as an IN (SELECT VALUE …)
membership check. A table whose permission fails to compile is never synced
(fail-closed), so a client can never receive rows an unrepresentable rule was
meant to guard.