Anonymous sign-ins
Rakomi supports anonymous sign-ins — a short-lived guest identity with no
email, no password, and a server-assigned opaque UUID. Use it when you want
users to try your app before they commit to creating an account; later they
can claim the guest identity by registering, and their data (cart,
preferences, metadata) carries over because users.id is preserved.
Guest vs. anonymous. Rakomi uses “anonymous” in the API and JWT claim (
is_anonymous: true) to match Firebase, Supabase, and Cognito parity. The user-facing term is guest — an opaque, time-bounded identity.
How it compares
Section titled “How it compares”| Provider | API | Upgrade path |
|---|---|---|
| Firebase | signInAnonymously() | linkWithCredential() |
| Supabase | Anonymous sign-ins (GA) | Update user with email |
| Cognito | GetId | MergeDeveloperIdentities |
| Rakomi | POST /v1/auth/anonymous | Register with anon bearer → claim preserves users.id |
Enterprise-only providers (Stytch, WorkOS) do not offer a first-class anonymous path. Rakomi supports guest sessions for consumer and B2C workloads while keeping the same tenant-isolated, cross-tenant-safe posture.
Enable it
Section titled “Enable it”Anonymous sign-ins are disabled by default on every new tenant. Enable them in the dashboard:
- Settings → Authentication → Anonymous.
- Toggle “Enable anonymous sign-ins” ON.
- Choose an inactive-user retention period (1–90 days, default 30).
If your tenant’s default role grants permissions beyond read-only, the
dashboard surfaces a red warning banner. Enabling the feature the first time
requires an explicit acknowledgement (soft-confirm with the
X-Rakomi-Confirm: privileged-default-role header — the dashboard wires this
for you). Anonymous users inherit the default role until they register, so the
recommendation is to keep the default role viewer-only.
Node SDK (@rakomi/node)
Section titled “Node SDK (@rakomi/node)”import { AnonymousSessionExpiredError, RakomiClient } from '@rakomi/node';
const client = new RakomiClient({ apiKey: process.env.RAKOMI_API_KEY! });
const result = await client.anonymous({ publicMetadata: { cart_id: 'c_123' },});if (!result.ok) { console.error(result.error.code); // anonymous/disabled | anonymous/rate_limited | anonymous/mau_exhausted return;}console.log(result.data.user.id); // opaque UUIDReact SDK (@rakomi/react)
Section titled “React SDK (@rakomi/react)”import { useAnonymousSignin, useAuth } from '@rakomi/react';
export function GuestCTA() { const { signIn, isLoading, error } = useAnonymousSignin(); const auth = useAuth(); if (auth.isLoaded && auth.isSignedIn && auth.user.isAnonymous) { return <p>You're a guest. <a href="/register">Create an account</a> to save your work.</p>; } return ( <button disabled={isLoading} onClick={() => signIn({ publicMetadata: { source: 'cta' } })}> Try it out </button> );}Security note.
useAuth().user.isAnonymousis derived from the verified JWT. Never infer “anonymous” from URL parameters, localStorage, or any untrusted client state — those paths are forgeable.
Direct HTTP
Section titled “Direct HTTP”curl -XPOST https://api.rakomi.com/v1/auth/anonymous \ -H 'Content-Type: application/json' \ -H "X-API-Key: $RAKOMI_API_KEY" \ -d '{}'# 201 Created → { access_token, refresh_token, expires_in, user: { id, is_anonymous: true, created_at } }Lifecycle
Section titled “Lifecycle”create → use → (refresh)* → claim → registered user │ │ └─── no activity (TTL days) ──────────┴── purge (hard delete)- Create —
POST /v1/auth/anonymous. Returns a JWT withis_anonymous: true,aal: "AAL1". - Use — authenticate as any other user.
/v1/auth/meworks with the anon bearer. - Refresh —
POST /v1/auth/refreshrotates the token.is_anonymous: truesurvives rotation. - Claim —
POST /v1/auth/registerwith the anon bearer sets the email + password and flipsis_anonymous: false. Theusers.idvalue is preserved, so all dependent rows carry over. - Purge — a daily cron at 02:30 UTC hard-deletes anonymous users whose
last_active_atis older than the tenant’s retention setting.
Concierge metaphor
Section titled “Concierge metaphor”An anonymous user is like a hotel walk-in with a temporary room key. Check-in at reception (register) turns the walk-in into a named guest without re-issuing room access. The guest’s belongings (metadata, cart) stay in the same room.
Operations
Section titled “Operations”Disabling the toggle affects new sessions only
Section titled “Disabling the toggle affects new sessions only”Turning off “Enable anonymous sign-ins” stops new guest creations. Existing guest tokens continue to refresh until they expire naturally or the purge cron removes the row at TTL. If you need to cut existing guests immediately, the operator user-deletion tool is the path.
Lowering TTL aggressively drains gradually
Section titled “Lowering TTL aggressively drains gradually”The purge cron caps at 1 000 rows per tenant per night to protect DB throughput. Lowering retention from 90 → 1 with 50 000 dormant guests drains over ~50 nights. Plan accordingly.
Rate limits
Section titled “Rate limits”- Per-IP: 5 requests per minute (same as
/v1/auth/register). - Per-API-key: 1 000 requests per hour — defense in depth against rotating
proxies. 429 responses include
Retry-AfterandX-Rakomi-Docs: https://docs.rakomi.dev/reference/auth/anonymous#rate-limits.
Refresh and key rotation
Section titled “Refresh and key rotation”- Anonymous tokens participate in normal JWT key rotation transparently.
- A refresh failing with 401 on a previously-anon token throws
AnonymousSessionExpiredErrorin the Node SDK (withsuggestedAction: 'call_anonymous()') so you can mint a fresh guest session instead of blanking the user out.
Cookies & consent
Section titled “Cookies & consent”The guest session cookie is a strictly-necessary authentication cookie —
no cookie-consent prompt is required under the current ePrivacy Directive
interpretation. The ROPA entry anonymous_user_creation documents the legal
posture.
SLA posture
Section titled “SLA posture”The purge cron is not business-critical under ISO 27001 A.5.30. Missing a run for ≤ 48 hours has no regulatory implication — the next successful run drains the backlog. This surface is in scope for the quarterly LAB security re-scan.
Claim paths
Section titled “Claim paths”| Path | Status |
|---|---|
| Email + password | Supported (Story 21.4b) |
| Social (OAuth) | Supported (Story 21.4-claim-social) — 10 providers |
| Passkey | Future work |
Claim via Social OAuth
Section titled “Claim via Social OAuth”A guest who started their session with POST /v1/auth/anonymous can upgrade to a
full account by signing in with a social provider. Send the anonymous bearer token
on the /oauth/{provider}/authorize initiate call; the server persists the anon
user id across the provider redirect and, on the callback, updates the SAME
users.id in place (preserving all FK-keyed data — sessions, metadata, org
memberships, audit trail).
Supported providers: google, apple (email always_verified); github,
microsoft, discord, slack, gitlab, linkedin
(provider_verified); facebook, twitter (never_trust — these do not
auto-link to an anon row; they create a fresh account instead, matching
Auth0/Firebase anti-takeover semantics for unverified provider emails).
import { RakomiClient } from '@rakomi/node';const client = new RakomiClient({ apiKey: process.env.RAKOMI_API_KEY! });
// 1. Create guest sessionconst guest = await client.auth.anonymous();
// 2. Redirect user's browser to the initiate route with the anon bearer.// The browser will follow 302 → Google → 302 back to /oauth/google/callback.// After the callback, the returned tokens identify the SAME users.id as `guest`,// but with is_anonymous: false and the Google federated identity linked.const initiateUrl = `https://accounts.rakomi.com/oauth/google/authorize?tenant_id=${tenantId}&redirect_uri=${redirectUri}`;// browser-side fetch with `Authorization: Bearer ${guest.access_token}` headerError redirects. Three Rakomi-extension error codes may land in the
?error= fragment on the callback redirect: already_claimed (the anon row was
already claimed in parallel), email_exists (the email returned by the provider
belongs to a different account), provider_in_use (this social account is
already linked to another Rakomi user). Localize these in your accounts app
(see Story 21.3 i18n keys).
My guest session disappeared — what happened? The tenant’s inactive-user retention period lapsed. Retention is configured in Settings → Authentication → Anonymous.
I got 429 on /v1/auth/anonymous — why?
Either the per-IP (5/min) or per-API-key (1 000/h) cap was hit. The response
carries Retry-After; back off accordingly.
Can I reset and start fresh?
Yes — on the browser, clear the session cookie / local tokens and call
client.anonymous() again. This creates a new guest with a new UUID.
Can a guest user belong to an organization? No. Organization membership requires a claimed identity.
What if an API key used to create the guest is revoked later?
The guest token remains valid for its exp. Refresh will fail because the
session’s API key is revoked — the user is signed out naturally.
What identifier is attached to me as a guest? An opaque UUID. No IP or User-Agent is stored on the user row. Server logs may retain short-lived access records per platform retention policy, same as any authenticated request.