# Authentication Source: https://docs.sublay.io/v7/authentication How Sublay handles user identity — built-in auth, external JWT integration, and OAuth. Sublay supports three authentication models. You choose the one that fits how your application manages users, and they can coexist within the same project. Sublay manages the full user lifecycle: sign-up, sign-in, and password reset using email and password. No external auth system needed. Sign in with Google, GitHub, Apple, or Facebook. Supports linking multiple providers to one account. Your backend signs a JWT. Sublay verifies it and creates or matches a user automatically. Keep your existing auth stack. *** ## Token Model All three auth modes produce the same token pair: an **access token** and a **refresh token**. | Token | Lifetime | Purpose | | ------------- | ---------- | ----------------------------------------------------------------- | | Access token | 30 minutes | Sent with every API request in the `Authorization: Bearer` header | | Refresh token | 30 days | Used to obtain a new access token when the current one expires | **Refresh token rotation** is enforced. Every time a refresh token is used, it is revoked and a new one is issued in its place. If the same refresh token is used twice (token reuse detected), the entire token family is revoked, signing out all sessions that shared it. A 30-second grace period allows two requests that race to refresh the same token (e.g., two browser tabs loading simultaneously) to both succeed. The first request rotates the token; a second request arriving within that window receives the successor token instead of a reuse error. The React/React Native SDK handles token storage and refresh automatically. When using the API directly, your code must exchange the refresh token for a new access token before each expiry. *** ## Built-in Auth Sublay provides a complete email/password authentication system out of the box. Use it when you do not have an existing user identity system and want Sublay to manage everything. **What it includes:** * User sign-up with email and password * Sign-in returning an access token + refresh token + user object * Sign-out (revokes the refresh token) * Password change (requires current password) * Password reset via email link (requires an email provider configured in the dashboard) Users created through built-in auth can link OAuth providers to their account later — see the OAuth section below. See [SDK Reference → Authentication → Built-in](/sdk/authentication/built-in) for implementation guidance, or [API Reference → Auth](/api-reference/auth/sign-up) for the raw endpoint reference. *** ## External Auth Use this model when your application already has its own user management system (Supabase Auth, Auth0, Firebase Auth, NextAuth, a custom system, or anything else) and you want Sublay to associate its data with those users — without rebuilding auth. ### How It Works Sublay generates an RSA key pair (RS256) for your project. The **public key** is stored in Sublay. The **private key** is shown to you once — store it securely as a server-side environment variable. If it is lost or compromised, generate a new key pair from the dashboard. When your application detects a logged-in user, make a request to your own backend. Your backend signs a JWT with your private key and returns it to the client. The JWT payload must include: | Claim | Value | | ---------- | ------------------------------------------- | | `sub` | Your external user ID (string) | | `iss` | Your Sublay project ID | | `userData` | Object with user profile fields (see below) | The `userData` object can include: | Field | Type | Notes | | ---------------- | -------- | ------------------------------------------------ | | `email` | `string` | Optional but recommended | | `name` | `string` | Display name | | `username` | `string` | Unique per project | | `avatar` | `string` | URL of the user's profile picture | | `bio` | `string` | Short bio | | `location` | `string` | User's location | | `birthdate` | `string` | ISO 8601 date | | `metadata` | `object` | Public custom data | | `secureMetadata` | `object` | Private custom data (not exposed to other users) | The signed JWT is safe to pass to the client — it cannot be tampered with because the signature covers the payload using your private key. Pass the signed JWT to `SublayProvider` via the `signedToken` prop (SDK), or call the verify external user API endpoint directly. Sublay verifies the JWT against the project's stored public key. If valid, it looks up the user by the `sub` claim. If the user does not exist, it is created from the `userData` fields. If the user already exists, their profile fields are updated if anything has changed. On success, Sublay returns its own access token + refresh token pair tied to this user. ### Profile Sync Every time a valid external JWT is presented, Sublay compares the `userData` fields against the stored user record and updates any fields that have changed. Your user profiles in Sublay stay in sync with your external system automatically — no separate update call needed. ### Security The private key must never leave your server. Signing happens server-side; the client only receives the finished JWT. Because Sublay verifies using your public key, a client cannot forge or modify user data. See [SDK Reference → Authentication → External](/sdk/authentication/external) for implementation guidance. *** ## OAuth OAuth allows users to sign in using a third-party provider. Sublay handles the full flow: PKCE generation, state management, callback processing, and user creation or linking. **Supported providers:** Google, GitHub, Apple, Facebook ### Setup Each provider must be configured in the project dashboard with: * The provider's client ID and client secret * A list of allowed redirect URIs (the URLs Sublay may redirect back to after authentication) ### Flow Overview Call the authorize endpoint with the provider name and the `redirectAfterAuth` URL. Sublay creates a short-lived state record (10-minute TTL) and returns an authorization URL pointing to the provider. For Google, a PKCE code verifier and challenge are generated automatically. Redirect the user to the authorization URL. They authenticate with the provider and are sent back to Sublay's callback endpoint. Sublay exchanges the authorization code for the provider's tokens, fetches the user profile, and resolves which Sublay user to return using this priority: 1. **Existing identity** — if this provider account has signed in before, the matching user is returned immediately. 2. **Verified email match** — if the provider supplies a verified email that matches an existing user, the new identity is linked to that account automatically. 3. **New user** — if no match is found, a new user is created from the provider profile (name, email, avatar). Sublay then redirects to the `redirectAfterAuth` URL with Sublay tokens appended as query parameters. Your app reads the tokens from the redirect URL, stores them, and the user is signed in. ### Account Linking A user can link multiple OAuth providers (and a password account) to a single Sublay user record. To link an additional provider, the user must be signed in, and the OAuth flow is initiated via the link endpoint rather than the authorize endpoint. The new identity is attached to the existing account. This enables scenarios like "sign in with Google on a laptop and GitHub on another device" — both map to the same user. See [SDK Reference → Authentication → OAuth](/sdk/authentication/oauth) for the full integration guide. *** ## Multi-Account Support The SDK supports multiple simultaneously signed-in accounts in a single app instance. This is separate from account linking — it means multiple distinct user sessions can be held at once, with the ability to switch between them. Use cases: apps where a person manages multiple accounts, shared devices, or testing flows. See [SDK Reference → Authentication → Multi-Account](/sdk/authentication/multi-account) for the full guide. *** ## Authorization Beyond authentication, Sublay enforces authorization on every request: * Most write operations require an authenticated user (or a service API key) * Ownership checks are enforced server-side — a user cannot modify another user's content through the normal API * Space roles (owner, admin, moderator, member) control what is permitted within a space * Service API keys bypass user-level authorization and are intended for server-side use only — see [Integration Options](/integration-options#service-api-key-recommended-for-server-to-server) # Bundles Source: https://docs.sublay.io/v7/bundles Sublay features are modular. Each project is assembled from bundles — groups of features you install per project and can add or remove at any time. Sublay is not one monolithic feature set. It's a collection of **bundles** — self-contained feature modules you turn on per project. A bundle groups everything a feature needs (its database tables and the endpoints and SDK hooks that operate on them), so your project only carries the features you actually use. Every project gets its own isolated database schema. Installing a bundle provisions that bundle's tables into your project's schema and switches the feature on. Removing a bundle takes it back out. Bundles are about **provisioning**, not pricing. Installing a bundle makes the feature available in your project; what each plan allows (quotas, AI access, etc.) is a separate concern. See [Semantic Search & AI](/semantic-search-ai) for an example of a feature that needs both its bundle *and* a paid plan. ## The core bundle Every project always has the **`core`** bundle. It's installed automatically when the project is created and cannot be removed. Core provides the foundation that every other feature builds on: * **Users, authentication & identity** — email/password accounts, OAuth identities, and OAuth provider configuration. See [Authentication](/authentication). Because OAuth identity lives in core, [OAuth sign-in](/sdk/authentication/oauth) works on every project without installing anything extra. ## Available bundles On top of core, you choose which of these to install. Most are independent — install only what you need — but a few depend on another bundle (see below). | Bundle | Feature | What it adds | | --------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `entities` | [Entities](/sdk/entities/overview) | The base content unit — posts, articles, listings, anything your users create — with views, drafts, and publishing | | `comments` | [Comments](/sdk/comments/overview) | Threaded comments on entities | | `reactions` | [Reactions](/sdk/reactions/overview) | Emoji reactions on entities and comments | | `files-images` | [Storage](/sdk/storage/overview) | File and image uploads, including user avatars and banners | | `follows` | [Follows](/sdk/relationships/overview) | Unidirectional follow relationships | | `connections` | [Connections](/sdk/relationships/overview) | Bidirectional connection (friend-style) requests | | `spaces` | [Spaces](/sdk/spaces/overview) | Hierarchical community spaces, membership, roles, and rules | | `workspaces` | [Workspaces](/sdk/workspaces/overview) | Self-nesting SaaS/team workspaces with invitations and per-member authority | | `chat` | [Chat](/sdk/chat/overview) | Real-time 1:1 and group conversations with message reactions | | `collections` | [Collections](/sdk/collections/overview) | User-owned bookmarking and folders for saving entities | | `moderation` | [Moderation](/sdk/moderation/overview) | Reports, report resolution, and user suspensions | | `notifications` | [App Notifications](/sdk/app-notifications/overview) | In-app notification system | | `push` | [Push Notifications](/push-notifications) | Native OS push delivery to iOS, Android, and Web | | `ai-search` | [Semantic Search & AI](/sdk/search/overview) | Content embeddings, semantic search, and the AI ask endpoint | | `reputation` | [Reputation](/data-models/reputation), [ReputationGrant](/data-models/reputation-grant) | Per-space reputation buckets with a maintained overall total on each user, plus reputation grants — reputation deliberately given by a user or minted by your app | ### Bundle dependencies A couple of bundles build directly on another and can't be installed on their own: * **`comments` and `collections` require `entities`.** Comments attach to an entity and collections save entities, so both need the `entities` bundle installed first. The dashboard pre-checks `entities` for you when you select either; installing them via the API without `entities` returns `409 database/bundle-prerequisite-missing`. For the same reason, `entities` can't be removed while `comments` or `collections` are still installed — remove those first. * **`interest-matching` requires `ai-search`.** Interest matching builds on the content embeddings that `ai-search` provides, so `ai-search` must be installed first and can't be removed while `interest-matching` is present. ## Choosing bundles when you create a project When you create a project in the [dashboard](https://dash.sublay.io), you pick the bundles you want. `core` is always included — you can't deselect it — and you can start with **core only** and add the rest later. Nothing is locked in at creation time. ## Adding and removing bundles later Installed bundles are managed from the **Database** page of your project in the dashboard (`https://dash.sublay.io//database`). From there you can install a bundle you skipped, or remove one you no longer need. Installing a bundle is an asynchronous operation — the dashboard shows it as **provisioning** while the tables are created, then **installed** once it's ready. This usually takes a moment. **Removing a bundle is destructive.** Uninstalling drops the bundle's tables and the data in them, and clears references to that data from other bundles. For example, removing the `spaces` bundle deletes all spaces and clears the `spaceId` on any entities, comments, or conversations that pointed at a space. There is no undo — back up anything you need first. Uninstalling the `reputation` bundle is doubly lossy: it drops the reputation buckets table and the grants table — destroying all grant history — **and** zeroes the `reputation` column on every user. Reinstalling starts fresh from zero — past scores are not recomputed. See [Reputation](/data-models/reputation). ## What happens if a bundle isn't installed Feature endpoints and SDK hooks only work when their bundle is installed. If your app calls a feature whose bundle is missing, the API responds with a clear error instead of a generic failure: | Situation | Response | Meaning | | --------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Bundle not installed | `403` · `database/tables-not-available` | The feature isn't enabled for this project. The response includes a `dashboardUrl` pointing at the project's Database page so you can install it. | | Bundle currently installing | `503` · `database/tables-provisioning` | The bundle is mid-install. This is transient — retry shortly. | If you hit one of these while building, the fix is almost always to install the bundle for the feature you're using. Open the **Database** page in the dashboard and add it. This is why a feature can be fully documented and supported but still return `database/tables-not-available` on your project — the docs describe the whole platform, while your project only runs the bundles you've installed. # Claude Code Plugin Source: https://docs.sublay.io/v7/claude-code Install the Sublay plugin for Claude Code to give your coding agent an accurate, built-in map of the Sublay platform. Sublay ships an official **Claude Code plugin** that installs a skill teaching your coding agent what the platform can do — every model, bundle, and capability — so it builds on what already exists instead of reinventing it or inventing what isn't there. This is complementary to the [MCP Server](/mcp-server). The **plugin** gives the agent a curated map of the platform that it reaches for automatically; the **MCP server** lets it pull exact field- and endpoint-level detail from the docs on demand. Installing the plugin also wires up the docs MCP for you. ## Install In Claude Code, add the marketplace and install the plugin: ```bash theme={null} /plugin marketplace add sublay-io/claude-skills /plugin install sublay@sublay-io ``` The first command registers Sublay's plugin marketplace; the second installs the `sublay` plugin from it. ## What you get A complete, domain-organized map of Sublay — the mental model (project scoping, bundles, the Provider + Hook SDK pattern), every bundle, and each model and capability (entities, spaces, chat, custom tables, storage, semantic search & AI, and more). The agent consults it automatically when you work on anything Sublay-related, so it picks the right SDK hook, REST route, or dashboard surface. The plugin bundles the public [docs MCP](/mcp-server) (`https://docs.sublay.io/mcp`, no auth), so when the agent needs exact hook signatures, params, or endpoint shapes, it can search the live docs without any extra setup. ## How it behaves The skill is a **first point of contact, not a full spec**. It holds the shape of the platform — what exists and why — and defers precise, field-level detail to the docs (via the MCP or [docs.sublay.io](https://docs.sublay.io)). That keeps the agent from guessing at APIs while staying lightweight. You don't need to invoke it manually — Claude Code loads it when your request involves Sublay. You can also call it explicitly with `/sublay:implementation-expert`. ## Updating When Sublay ships new skills or updates, run: ```bash theme={null} /plugin marketplace update sublay-io ``` then reinstall or update the plugin to pick up the latest version. # Custom Tables Source: https://docs.sublay.io/v7/custom-tables Provision your own database tables for the remaining 20% of your data model — with SDK row access, table management, and a full dashboard data editor Sublay ships pre-modeled tables for the features you'd otherwise have to build — users, entities, comments, spaces, reactions, and more. **Custom tables** cover the rest: the app-specific data that doesn't fit any built-in model. You define the table, Sublay provisions it inside your project's isolated schema, and you read and write its rows from the SDKs or the dashboard. Custom tables live in the same per-project schema as your built-in data, so they share your project's isolation and connection. They are addressed through a dedicated `/db` surface that **only ever touches custom tables** — built-in and bundle tables are never reachable through it. ## The `custom_` invisibility model Every custom table is stored with a `custom_` prefix on its physical name, but you never type that prefix. You work with **logical** names everywhere: * You create a table called `Events` → Sublay stores it physically as `custom_Events`. * You read and write rows by calling `client.table("Events")` → Sublay resolves it to `custom_Events`. * The dashboard displays it as `Events`. The prefix is what guarantees the `/db` surface can never reach a built-in table. A request for a built-in name like `Comments` resolves to `custom_Comments`, which doesn't exist, so it `404`s — the built-in `Comments` table stays unreachable. The prefix is applied **exactly once, unconditionally** — it is never skipped if your name already starts with `custom_`. If you name a logical table `custom_x`, it is physically stored as `custom_custom_x` and you continue to address it as `custom_x`. There is no double-prefix surprise and no name collision with the prefix mechanism. ## Managed columns Every custom table is created with a set of **server-managed columns** that you never write to directly: | Column | Type | When present | Purpose | | ----------- | ------------- | ----------------------- | ---------------------------------------------- | | `id` | `uuid` | Always | Primary key, defaulted to `gen_random_uuid()`. | | `createdAt` | `timestamptz` | When `timestamps: true` | Set on insert. | | `updatedAt` | `timestamptz` | When `timestamps: true` | Bumped automatically on every update. | | `deletedAt` | `timestamptz` | When `paranoid: true` | Soft-delete marker. `null` for live rows. | These columns are reserved at create time and at add-column time — you cannot define a column with any of these names. They are rejected from insert/update bodies (a write that includes `id`, `createdAt`, `updatedAt`, or `deletedAt` is refused). `deletedAt` is **read-visible** — you can filter and sort by it and surface it with `includeDeleted` — but never writable. ### Timestamps and soft-delete Two flags, set when you create the table, control the managed columns: * **`timestamps`** (default `true`) — emits `createdAt` / `updatedAt`. With timestamps on, the default sort for reads is `createdAt desc`; with timestamps off, the default sort is `id`. * **`paranoid`** (default `false`, **requires `timestamps`**) — emits `deletedAt` and turns deletes into **soft deletes**. A delete sets `deletedAt` instead of removing the row; soft-deleted rows are excluded from reads by default and resurface only with `includeDeleted`. A [`restore`](/v7/node-sdk/tables#restore) clears `deletedAt`. Pass `force: true` to a delete to hard-delete a paranoid row. A non-paranoid table has no `deletedAt` column, so every delete is a hard delete. ## Column types A custom column is one of nine logical types: | Logical type | Physical SQL type | Notes | | ------------ | ------------------ | --------------------------- | | `text` | `TEXT` | | | `integer` | `BIGINT` | 64-bit integer. | | `float` | `DOUBLE PRECISION` | | | `decimal` | `NUMERIC` | Arbitrary-precision number. | | `boolean` | `BOOLEAN` | | | `date` | `DATE` | Calendar date, no time. | | `timestamp` | `TIMESTAMPTZ` | Timestamp with time zone. | | `uuid` | `UUID` | | | `json` | `JSONB` | | Each column carries a `nullable` flag and an optional `defaultValue` (validated against the column's type). ## Surfaces Custom tables are reachable from every Sublay SDK and from the dashboard: `client.table(name)` — find, create, update, delete, bulk, restore. `client.tables` — create/drop tables and add/drop columns (service-key only). `client.table(name)` row operations from any browser or JS runtime. `useTable(name)` for React and React Native. Table **management** (DDL — creating tables, adding columns) is available in two places only: the **Node SDK** (service-key) and the **dashboard**. The JS SDK and the React hook are row-only, because they authenticate as an end user and hold no service key. ## The dashboard data editor The dashboard ships a full data editor for custom tables, under the project's **Database** view: * **Schema management** — create a table (with the full type set, per-column nullable/default, and the `timestamps` / `paranoid` toggles), add a column to an existing table, and drop tables or columns. Drop actions open a confirmation dialog that previews what will be lost (e.g. the affected row count) and gate the action behind typing the table/column name. * **Row editing** — insert and edit rows through type-aware inputs. Managed columns are not editable. (Row editing is offered for custom tables only — built-in tables stay read-only in the editor.) * **Soft delete & restore** — on a paranoid table, the grid offers a **Show deleted** toggle; soft-deleted rows render dimmed with a **Restore** action. * **Filter, sort, paginate** — browse rows with the same filter/sort/pagination contract the SDKs use. Custom table names display with the `custom_` prefix stripped (`Events`, not `custom_Events`). **Permissions.** Schema changes (create/drop table, add/drop column) require an **owner or admin** role. Row writes (insert, edit, restore) require **owner or editor** — the same gate as deleting a row. ## Limits To keep a runaway script from exhausting your schema, custom tables are bounded: * **100 tables** per project. * **100 columns** per table. * **100 rows** per bulk create or bulk delete call. These ceilings are generous for legitimate data models and far below Postgres's own limits. ## Security: open row CRUD **Row CRUD on the `/db` surface is currently open.** The row endpoints capture the caller's identity (user token, service key) but **do not enforce any authorization** — any caller who can reach your project can read and write custom-table rows. There are no row-, field-, or table-level policies yet. This is a deliberate, pre-release state. A hard authorization gate is planned before general availability, and the captured identity context is the integration point that policy layer will build on. **Do not store data in custom tables that requires per-row or per-user access control until that gate ships.** Table **management** (DDL) is not open — it requires a service key (SDK) or an owner/admin role (dashboard). # Use accounts Source: https://docs.sublay.io/v7/hooks/auth/use-accounts List all accounts linked in the current app instance ## Overview `useAccounts` returns the list of all stored accounts and the currently active account. Use it to render an account switcher or display the signed-in user's identity. ## Usage Example ```tsx React theme={null} import { useAccounts } from "@sublay/react-js"; function AccountSummaryBar() { const { accounts, activeAccount, accountCount } = useAccounts(); return (

Signed in as: {activeAccount?.name ?? activeAccount?.email ?? "Guest"}

Total accounts: {accountCount}

    {accounts.map((account) => (
  • {account.name ?? account.email}
  • ))}
); } ``` ```tsx React Native theme={null} import { useAccounts } from "@sublay/react-native"; function ActiveAccountLabel() { const { activeAccount } = useAccounts(); return {activeAccount?.name ?? "Not signed in"}; } ```
## Returns Array of all stored accounts, derived from the accounts map in Redux state. Each one carries the profile summary, the two credential markers below, and the `needsPushRebind` notification marker. The account currently **selected**, or `null` when nothing is selected. Selection and session are not the same thing, and the gap between them is a state your UI has to render. An account can be selected with no live session behind it — during an [`addAccount()`](/hooks/auth/use-add-account) flow, or after a sign-in was refused at the account limit. Read `accessToken` from [`useAuth`](/hooks/auth/use-auth) for whether a session exists; read this for whose account the app is pointed at. Total number of stored accounts. `true` when there is no active account **because the user deliberately signed out** — or removed the active account, deleted it, or had a stored credential refused at launch — as opposed to "nothing has ever been selected". Both look like `activeAccount === null`; this is what tells them apart, and it is persisted, so it survives a relaunch. A launch that could not *reach* the server does not set it: the stored account stays selected and its session is restored later. Only a refusal counts. While it is `true`, the SDK will not auto-select a stored account at launch. It clears as soon as any account is successfully activated. `true` when an account was actually refused admission because the map was already at the 5-account limit. Clears on the next successful admission and on any removal. See [`useAddAccount`](/hooks/auth/use-add-account) for how it differs from `canAddAccount` and from [`wouldExceedAccountLimit`](/sdk/authentication/multi-account#three-signals-for-the-account-cap). **Render off it — do not read it eagerly.** The clear is dispatched from an effect, so it lands one render *after* a successful admission rather than synchronously inside the call that caused it. Reading it immediately after `await`ing a sign-in returns the value from before that effect flushed, which may be a `true` left over from an earlier, unrelated refusal. See [When `accountLimitReached` clears](/sdk/authentication/multi-account#when-accountlimitreached-clears). ## StoredAccount Type | Property | Type | Description | | ----------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | User ID | | `name` | `string \| null` | Display name | | `username` | `string \| null \| undefined` | Username. `undefined` on entries stored by an SDK version before this field existed — absent means *unknown*, not "no username" | | `email` | `string \| null` | Email address | | `avatar` | `string \| null` | Avatar URL | | `tokenExpiresAt` | `number` | When this account's stored refresh token expires, as an epoch in **milliseconds**. `0` means unknown | | `needsReauth` | `boolean` | `true` once a transition into this account has actually been refused — the credential is dead | | `needsPushRebind` | `boolean` | `true` when this account's notifications are paused until it is next opened | `StoredAccount` is a superset of the `AccountSummary` shape stored in the account map, so code that only reads `id`/`name`/`username`/`email`/`avatar` is unaffected. ## Telling a Live Account From a Dead One A stored account can stop working while it is sitting in the switcher. Two fields let you show that **before** the user taps it, and you need both — neither is sufficient on its own: **Proactive.** Read from the stored refresh token's own `exp` claim when the entry is written (a decode, never a verification and never a network call). `0` means *unknown* — the token carried no readable numeric `exp`, so the SDK recorded nothing rather than guessing. It sorts as already-expired deliberately: treating a credential the SDK cannot read as fresh is the worse failure. **Reactive.** Set when a transition into this account is refused — by a switch, or by the automatic restore at app launch, which is the most common way a revoked credential is discovered. Cleared the moment the account is successfully activated again, whether by a switch that works or by signing into it afresh. This catches every death `tokenExpiresAt` cannot see: reuse detection, a password change, a remote sign-out-all, an admin revocation. All of those destroy the token family while `exp` is still comfortably in the future. It is also set without any network call when the stored entry carries no usable credential at all, which is the same conclusion reached sooner. A failed switch that could not reach the server does **not** set it — a flaky network is not a dead account. `AccountTransitionError.credentialRejected` is the same distinction, reported to the caller at the moment of failure; see [`useSwitchAccount`](/hooks/auth/use-switch-account#telling-a-dead-account-from-a-dead-network). ```tsx theme={null} const { accounts } = useAccounts(); const needsSignIn = (account: StoredAccount) => account.needsReauth || account.tokenExpiresAt <= Date.now(); accounts.map((account) => ( )); ``` ### Notifications paused: `needsPushRebind` `true` when this account's push binding points at a device token this device no longer holds, because the token rotated while the account was in the background. Its credential is fine and its data is fine — only the notification routing is stale. Switching into the account re-creates the binding and clears this, so the useful thing to render is an invitation to open it rather than a sign-in prompt: *"Notifications paused — open to resume."* Only ever raised for accounts that explicitly enabled push on this device. An account that never asked for notifications has none to pause. **Do not conflate it with `needsReauth`.** They mean opposite things about the account's health, and saying the wrong one is wrong in both directions: telling a user to sign in again when their notifications are merely stale asks for a password they do not need, and treating a paused account as fine hides the one thing a tap would fix. Neither credential marker is a promise: `needsReauth: false` with a future `tokenExpiresAt` means *nothing has gone wrong that the SDK knows of*, not that the next switch is guaranteed to succeed. A switch can still fail, and when it does the current session is left untouched — see [When a transition fails](/sdk/authentication/multi-account#when-a-transition-fails). On Expo, `avatar` and then `email` may be dropped from a stored entry if it would otherwise exceed SecureStore's per-value limit. `id`, `name`, `username`, the credential and the markers are never dropped. See [Where accounts are stored](/sdk/authentication/multi-account#where-accounts-are-stored). ## Rendering the Signed-Out State No account is auto-selected after a sign-out, removal or deletion, so `activeAccount` can be `null` while `accounts` is non-empty. That is the account-picker state: ```tsx theme={null} import { useAccounts, useAuth } from "@sublay/react-js"; const { initialized, accessToken } = useAuth(); const { accounts, signedOut } = useAccounts(); if (!initialized) return ; if (accessToken) return ; if (signedOut && accounts.length > 0) return ; return ; ``` Branch on `accessToken`, not on `activeAccount`. A selection is not a session: `addAccount()` leaves the previous account selected while it clears the session, so a selection-based gate would render your app for an account that has no tokens behind it. `signedOut` then separates the picker state from a device where nobody has ever signed in. ## Integration Guide For multi-account integration guidance, see [Multi-Account](/sdk/authentication/multi-account). # Add account Source: https://docs.sublay.io/v7/hooks/auth/use-add-account Link a new user account to the current app instance ## Overview `useAddAccount` returns an `addAccount` function that clears the active auth state, allowing the user to sign into a new account. Previously stored accounts are not removed — they remain in the accounts map and can be restored with `useSwitchAccount`. After the user completes sign-in, the new account is added to the accounts map automatically. 5 accounts can be stored simultaneously. `canAddAccount` is `false` when the limit is reached. All 5 genuinely persist on every platform — see [Where accounts are stored](/sdk/authentication/multi-account#where-accounts-are-stored). ## Usage Example ```tsx React theme={null} import { useAddAccount } from "@sublay/react-js"; function AddAccountButton() { const { addAccount, canAddAccount } = useAddAccount(); return ( ); } ``` ```tsx React Native theme={null} import { useAddAccount } from "@sublay/react-native"; function AddAccountButton() { const { addAccount, canAddAccount } = useAddAccount(); return ( ); } return ( ); } ``` ```tsx React Native theme={null} import { useAuth } from "@sublay/react-native"; function SignInButton() { const { initialized, accessToken, signInWithEmailAndPassword } = useAuth(); if (!initialized) return null; if (accessToken) return null; return ( ); } ``` ```tsx React Native theme={null} import { useConfirmAccountDeletion } from "@sublay/react-native"; function ConfirmDeletionScreen({ code }: { code: string }) { const confirmAccountDeletion = useConfirmAccountDeletion(); return ( ))} ); } ``` ## Returns Array of OAuth identities linked to the current user. Initially empty; call `fetchIdentities` to populate. Whether the user has a password set. `false` for OAuth-only accounts, and until `fetchIdentities` has run. Use it to prompt the user to [set an initial password](/hooks/auth/use-auth) (via `useAuth().setPassword`) before they unlink their last identity. Async function that fetches the identities list from the server and updates local state. Returns `void`. Async function that unlinks a specific identity. Accepts `{ identityId: string }`. On success, removes the identity from the local `identities` array. Fails if it would be the last identity and the user has no password set. `true` while a fetch or unlink request is in progress. Error message from the most recent failed operation, or `null` if no error. ## OAuthIdentity Type | Property | Type | Description | | ------------------- | ---------------- | -------------------------------------------------- | | `id` | `string` | Unique identity ID (UUID) | | `provider` | `string` | OAuth provider name (e.g., `"google"`, `"github"`) | | `providerAccountId` | `string` | The user's ID at the provider | | `email` | `string \| null` | Email returned by the provider | | `name` | `string \| null` | Name returned by the provider | | `avatar` | `string \| null` | Avatar URL returned by the provider | | `isVerified` | `boolean` | Whether the provider marked the email as verified | | `createdAt` | `string` | ISO timestamp of when the identity was linked | ## See Also * [OAuth integration guide](/sdk/authentication/oauth) * [List Identities API reference](/api-reference/oauth/list-identities) * [Unlink Identity API reference](/api-reference/oauth/unlink-identity) # Use OAuth sign-in Source: https://docs.sublay.io/v7/hooks/auth/use-oauth-sign-in Initiate OAuth sign-in, link OAuth providers, and handle the callback — web only ## Overview `useOAuthSignIn` handles the full redirect-based OAuth flow for web apps. It provides three functions: * **`initiateOAuth`** — redirect an unauthenticated user to the provider's authorization page to sign in or sign up. * **`linkOAuthProvider`** — redirect an already-authenticated user to link an additional OAuth provider to their account. * **`handleOAuthCallback`** — call this on the OAuth callback page to extract tokens from the URL fragment and set the authenticated session. This page documents the `@sublay/react-js` hook, which uses `window.location` for the redirect-based web flow. For Expo apps, `@sublay/expo` exports a `useOAuthSignIn` hook with the **same API** that opens the system browser and returns via a deep link — see [OAuth Authentication → Expo Integration](/sdk/authentication/oauth#expo-integration). ## Usage Example **Initiating sign-in:** ```tsx theme={null} import { useOAuthSignIn } from "@sublay/react-js"; function SignInPage() { const { initiateOAuth, isLoading, error } = useOAuthSignIn(); return ( <> {error &&

{error}

} ); } ``` **Handling the callback:** ```tsx theme={null} import { useEffect } from "react"; import { useOAuthSignIn } from "@sublay/react-js"; function OAuthCallbackPage() { const { handleOAuthCallback } = useOAuthSignIn(); useEffect(() => { handleOAuthCallback(); }, []); return

Signing you in...

; } ``` **Linking an additional provider:** ```tsx theme={null} import { useOAuthSignIn } from "@sublay/react-js"; function LinkGitHub() { const { linkOAuthProvider } = useOAuthSignIn(); return ( ); } ``` ## Returns Starts the OAuth sign-in / sign-up flow for unauthenticated users. Redirects the browser to the provider's authorization page. **Parameters:** * `provider` (required) — OAuth provider identifier (e.g. `"google"`, `"github"`). * `redirectAfterAuth` (optional) — URL to redirect to after authentication. Defaults to the current page URL. Links an additional OAuth provider to the currently authenticated user. The user must already be signed in. Redirects the browser to the provider's authorization page. **Parameters:** * `provider` (required) — OAuth provider identifier (e.g. `"google"`, `"github"`). * `redirectAfterAuth` (optional) — URL to redirect to after linking. Defaults to the current page URL. Reads tokens from the URL fragment (`#accessToken=...&refreshToken=...`) after the provider redirects back to your app. On success, stores the tokens and initializes the session. Returns `true` if tokens were found and set, `false` otherwise. Call this inside a `useEffect` on your callback page. It is **synchronous and returns before the user's identity is known**, so a `true` return means "tokens were found", not "this account was admitted". If the device already stores 5 accounts and this is a sixth, the session is signed back out and `accountLimitReached` is raised a moment later. `true` while the authorization request is in progress. Stays `true` after the redirect is triggered (since the page navigates away). Error message if the OAuth flow fails, or `null` when there is no error. **The account limit does not appear here.** Reaching the 5-account limit is discovered after the provider returns, and this hook has no call left to reject — read `accountLimitReached` from [`useAccounts`](/hooks/auth/use-accounts) or [`useAddAccount`](/hooks/auth/use-add-account) instead. This is the one place OAuth differs from email sign-in and external auth, which do throw. See [OAuth → The account limit](/sdk/authentication/oauth#the-account-limit). ## Integration Guide For full OAuth setup instructions, see [OAuth Authentication](/sdk/authentication/oauth). # Remove account Source: https://docs.sublay.io/v7/hooks/auth/use-remove-account Remove a linked account from the current app instance ## Overview `useRemoveAccount` signs out and removes a specific account from the stored accounts map. It sends a sign-out request to the server to revoke the account's refresh token family — and, once this device has a stored push identifier, to unbind that account's push notifications in the **same transaction** — before removing any local state. If the removed account is the currently active one, **the session ends and no account is left active**, even when other accounts remain stored. The app renders signed-out; choosing what happens next is yours. **Removal is atomic once push is involved.** If this device has a stored push identifier, the sign-out request carries it, and if the server **refuses the unbind** nothing is torn down: `removeAccount` **rejects**, `error` carries the reason, and the account keeps its entry and its credential so the user can retry. Its signature is `({ userId }) => Promise`, so nothing warns you at compile time: `await removeAccount(...)` needs a `catch`. The strictness is scoped to a **server refusal of the unbind itself** — the sign-out endpoint answering `auth/device-deregistration-failed` or `auth/sign-out-failed`, the two codes it returns when its transaction rolled back. Everything else is best-effort: no network, a throttled or migrating project, a rejected body, the generic `auth/server-error`, or a device with no stored push identifier. An offline user can still remove an account locally. One more code arrives on a **success** and never blocks: `auth/push-unbind-status-unknown`, returned with `200` when the server signed the account out without attempting an unbind because it could not determine whether the project has push devices. `removeAccount` resolves, the account is removed, and the SDK warns — the binding may still be live, so it is worth surfacing, but it is not worth making an account unremovable over. A stored push identifier is **device** state, not an account's consent: on native the SDK records one when the device already holds OS notification permission, whether or not this particular account ever registered. When the account had no binding, the unbind removes nothing and the removal completes exactly as it would without one. Removing an account never activates a different one. Read `activeAccount` from [`useAccounts`](/hooks/auth/use-accounts) and render your own next screen when it is `null`. ## Usage Example ```tsx React theme={null} import { useAccounts, useRemoveAccount } from "@sublay/react-js"; function AccountList() { const { accounts } = useAccounts(); const { removeAccount, isRemoving, error } = useRemoveAccount(); return (
    {accounts.map((account) => (
  • {account.name ?? account.email}
  • ))} {error &&

    {error}

    }
); } ``` ```tsx React Native theme={null} import { useAccounts, useRemoveAccount } from "@sublay/react-native"; function AccountItem({ userId }: { userId: string }) { const { removeAccount, isRemoving } = useRemoveAccount(); return ( ; } ``` ```tsx React Native theme={null} import { useRequestAccountDeletion } from "@sublay/react-native"; function DeleteAccountButton({ onCodeSent }: { onCodeSent: () => void }) { const requestAccountDeletion = useRequestAccountDeletion(); return ( ); } ``` ```tsx React Native theme={null} import { useRequestPasswordReset } from "@sublay/react-native"; function ForgotPasswordButton({ email }: { email: string }) { const requestPasswordReset = useRequestPasswordReset(); return ( ); } ``` ```tsx React — clickable link theme={null} import { useSendVerificationEmail } from "@sublay/react-js"; function VerifyEmailPrompt() { const sendVerificationEmail = useSendVerificationEmail(); return ( ); } ``` ```tsx React Native theme={null} import { useSendVerificationEmail } from "@sublay/react-native"; function VerifyEmailPrompt() { const sendVerificationEmail = useSendVerificationEmail(); return ( ; } ``` ```tsx React Native theme={null} import { useSignOutAll } from "@sublay/react-native"; function SignOutAllButton() { const { signOutAll } = useSignOutAll(); const handlePress = async () => { try { await signOutAll(); } catch (err) { // Some accounts were kept — surface the reason and let the user retry. showError(err); } }; return )} ))} {error &&

{error}

} ); } ``` ```tsx React Native theme={null} import { useSwitchAccount } from "@sublay/react-native"; function SwitchButton({ userId }: { userId: string }) { const { switchAccount, isSwitching } = useSwitchAccount(); return ( ); } ``` ```tsx React Native theme={null} import { useVerifyEmail } from "@sublay/react-native"; function VerifyCodeScreen({ code }: { code: string }) { const verifyEmail = useVerifyEmail(); return ( ; } ``` ### With image upload ```tsx theme={null} const entity = await createEntity({ title: "Photo post", images: { files: [imageFile], options: { width: 1200, height: 800, fit: "cover" }, }, }); ``` ## Parameters An ID from your own system to link this entity to an existing item. An identifier for grouping entities by source (e.g., a section of your app). The ID of the space this entity belongs to. Entity title. Entity body content. Array of attachment objects. Flexible structure — can represent media URLs, file metadata, or any structured data. Tags or keywords for filtering and discovery. Users mentioned in the entity content. Geographic location for the entity. Arbitrary project-specific data. Limited to 10 KB. Marks the entity's own NSFW flag. Defaults to `false`. This is only the entity's own flag — its effective NSFW status also inherits from its space. Sublay labels and filters NSFW content but does not blur or age-gate it. See [Entity NSFW flagging](/data-models/entity#nsfw-flagging). If `true`, creates the entity as a draft (not publicly visible). Default `false`. If `true`, the authenticated user is not associated as the entity author. If `true`, the request fails if no authenticated user is present. Images to upload and attach. Triggers a multipart form request. Files to upload and attach. Triggers a multipart form request. ## Returns The newly created entity. See [Entity data model](/data-models/entity). # Use entity Source: https://docs.sublay.io/v7/hooks/entities/use-entity Access entity state and actions from EntityProvider **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useEntity` reads from the nearest `EntityProvider` in the component tree and returns the entity's state and actions. It does not fetch data itself — it accesses what `EntityProvider` has already loaded. `useEntity` must be used inside an `EntityProvider`. See [EntityProvider & useEntity](/sdk/entities/provider-and-hook) for setup instructions. ## Usage Example ```tsx theme={null} import { useEntity } from "@sublay/react-js"; function EntityContent() { const { entity, updateEntity, deleteEntity } = useEntity(); if (!entity) return

Loading...

; return (

{entity.title}

Views: {entity.views}

); } ``` ## Return Values The current entity. `undefined` while loading, `null` if not found. Direct state setter for the entity. Use for manual local state overrides. Updates the entity on the server and syncs local state. Accepts `title`, `content`, `attachments`, `keywords`, `location`, `metadata`, and `mentions`. Deletes the entity and sets local state to `undefined`. # Fetch entity Source: https://docs.sublay.io/v7/hooks/entities/use-fetch-entity Fetch a single entity by its Sublay entity ID **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useFetchEntity` returns a function that fetches a single entity by its Sublay entity ID. Use this for on-demand fetches outside of `EntityProvider`. ## Usage Example ```tsx theme={null} import { useFetchEntity } from "@sublay/react-js"; function EntityLoader({ entityId }: { entityId: string }) { const fetchEntity = useFetchEntity(); const load = async () => { const entity = await fetchEntity({ entityId, include: ["user", "topComment"] }); console.log(entity); }; return ; } ``` ## Parameters The Sublay entity ID to fetch. Optional. Populate related fields. Accepted values: `"user"`, `"space"`, `"topComment"`, `"saved"`, `"files"`, `"grants"`. ## Returns The fetched entity. When `spaceReputation` is requested, the embedded author carries an added `spaceReputation` number. See [Entity data model](/data-models/entity) and [Reputation](/data-models/reputation). # Fetch entity by foreign ID Source: https://docs.sublay.io/v7/hooks/entities/use-fetch-entity-by-foreign-id Fetch a single entity by its foreign ID **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useFetchEntityByForeignId` returns a function that fetches an entity using the foreign ID from your system. Optionally auto-creates the entity if it does not yet exist. ## Usage Example ```tsx theme={null} import { useFetchEntityByForeignId } from "@sublay/react-js"; function ArticleSocial({ articleId }: { articleId: string }) { const fetchEntity = useFetchEntityByForeignId(); const load = async () => { const entity = await fetchEntity({ foreignId: articleId, createIfNotFound: true, }); console.log(entity); }; return ; } ``` ## Parameters The foreign ID to look up. This is the identifier from your own system. If `true`, automatically creates a new entity with this foreign ID when one does not exist. Optional. Populate related fields. Accepted values: `"user"`, `"space"`, `"topComment"`, `"saved"`, `"files"`, `"grants"`. ## Returns The fetched (or newly created) entity. See [Entity data model](/data-models/entity). # Fetch entity by short ID Source: https://docs.sublay.io/v7/hooks/entities/use-fetch-entity-by-short-id Fetch a single entity by its auto-generated short ID **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useFetchEntityByShortId` returns a function that fetches an entity by its `shortId` — the auto-generated short identifier that Sublay assigns to every entity. Short IDs are useful for building readable sharing URLs. ## Usage Example ```tsx theme={null} import { useFetchEntityByShortId } from "@sublay/react-js"; import { useParams } from "react-router-dom"; function SharePage() { const { shortId } = useParams<{ shortId: string }>(); const fetchEntity = useFetchEntityByShortId(); const load = async () => { const entity = await fetchEntity({ shortId }); console.log(entity); }; return ; } ``` ## Parameters The short ID to look up. This is the value from `entity.shortId`. Optional. Populate related fields. Accepted values: `"user"`, `"space"`, `"topComment"`, `"saved"`, `"files"`, `"grants"`. ## Returns The fetched entity. See [Entity data model](/data-models/entity). # Fetch many entities Source: https://docs.sublay.io/v7/hooks/entities/use-fetch-many-entities Fetch a paginated, filtered, and sorted list of entities **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useFetchManyEntities` returns a function that fetches a paginated list of entities with full filtering and sorting support. This is the low-level hook underlying `EntityListProvider`. For continuous-scroll or load-more list UIs, use [`useEntityList`](/hooks/entity-lists/use-entity-list) instead, which wraps this hook with built-in pagination state. ## Usage Example ```tsx theme={null} import { useFetchManyEntities } from "@sublay/react-js"; import { useEffect, useState } from "react"; function EntityFeed() { const fetchEntities = useFetchManyEntities(); const [entities, setEntities] = useState([]); useEffect(() => { fetchEntities({ page: 1, limit: 20, sortBy: "score", spaceId: "space_abc", include: ["user"], }).then((res) => setEntities(res.data)); }, []); return (
    {entities.map((e) => (
  • {e.title}
  • ))}
); } ``` ## Parameters Page number (1-indexed). Defaults to `1`. Results per page. Defaults to server default. Field to sort by. Options: `"createdAt"`, `"top"`, `"hot"`, `"controversial"`, or `"metadata."` to sort by a metadata field. Sort by a specific reaction type count. Use with `sortBy: "reaction"`. Sort direction: `"asc"` or `"desc"`. Sort algorithm type. Options: `"auto"`, `"numeric"`, `"text"`, `"boolean"`, `"timestamp"`. Used when sorting by a metadata field. Filter entities to a time window. Options: `"day"`, `"week"`, `"month"`, `"year"`. Filter to entities associated with a specific source ID. Filter to entities in a specific space. Filter to entities created by a specific user. If `true`, return only entities from users the authenticated user follows. Filter by keywords. See [Entity List Filters](/sdk/entity-lists/filters) for the full filter schema. Filter by title content. See [Entity List Filters](/sdk/entity-lists/filters). Filter by body content. See [Entity List Filters](/sdk/entity-lists/filters). Filter by attachments data. See [Entity List Filters](/sdk/entity-lists/filters). Filter by geographic proximity. See [Entity List Filters](/sdk/entity-lists/filters). Filter by metadata fields. See [Entity List Filters](/sdk/entity-lists/filters). Filter by **effective** NSFW status (`entity.nsfw OR its space's nsfwEffective`). `"include-all"` (default) adds no predicate; `"exclude"` drops effective-NSFW entities; `"only"` returns only effective-NSFW entities. On projects without the spaces bundle it applies against the entity's own `nsfw` flag alone. See [Entity NSFW flagging](/data-models/entity#nsfw-flagging). Populate related fields. Accepted values: `"user"`, `"space"`, `"topComment"`, `"saved"`, `"files"`, `"grants"`. ## Returns Array of entities for the current page. When `spaceReputation` is requested, each embedded author carries an added `spaceReputation` number. See [Reputation](/data-models/reputation). Current page number. Number of results per page. Total number of pages matching the filters. Total count of entities matching the filters (across all pages). `true` if there are additional pages available. # Use entities Source: https://docs.sublay.io/v7/hooks/entities/use-fetch-many-entities-wrapper Stateful, paginated entity feed with sorting and filtering **Requires the `entities` bundle.** A project without the entities bundle returns `403 database/tables-not-available`. See [Bundles](/bundles). ## Overview `useFetchManyEntitiesWrapper` is a stateful wrapper around [`useFetchManyEntities`](/hooks/entities/use-fetch-many-entities) that manages pagination, sorting, and list state for you. Use it when you need a custom entity feed outside of `EntityListProvider` — for example, a user profile feed, a filtered search results page, or any custom list with infinite scroll. All filters and sort options mirror those of `useFetchManyEntities`. Changing any sort option automatically resets the list and re-fetches from page 1. ## Usage Example ```tsx theme={null} import { useFetchManyEntitiesWrapper } from "@sublay/react-js"; function UserFeed({ userId }: { userId: string }) { const { entities, loading, hasMore, sortBy, setSortBy, loadMore, } = useFetchManyEntitiesWrapper({ userId, limit: 15, defaultSortBy: "createdAt", include: ["user"], }); return (
    {entities.map((entity) => (
  • {entity.title}
  • ))}
{hasMore && ( )}
); } ``` ## Parameters Number of entities per page. Default: `10`. Filter to entities created by a specific user. Filter to entities associated with a specific source ID. Filter to entities in a specific space. If `true`, return only entities from users the authenticated user follows. Populate related fields. Accepted values: `"user"`, `"space"`, `"topComment"`, `"saved"`, `"files"`, `"grants"`. Initial sort field. Default: `"createdAt"`. Initial reaction type to sort by when `sortBy` is reaction-based. Default: `"upvote"`. Initial sort direction. Default: `"desc"`. Initial sort algorithm. Options: `"auto"`, `"numeric"`, `"text"`, `"boolean"`, `"timestamp"`. Used when sorting by a metadata field. Default: `"auto"`. Filter entities to a time window. Options: `"day"`, `"week"`, `"month"`, `"year"`. Filter by keywords. See [Entity List Filters](/sdk/entity-lists/filters). Filter by title content. See [Entity List Filters](/sdk/entity-lists/filters). Filter by body content. See [Entity List Filters](/sdk/entity-lists/filters). Filter by attachments data. See [Entity List Filters](/sdk/entity-lists/filters). Filter by geographic proximity. See [Entity List Filters](/sdk/entity-lists/filters). Filter by metadata fields. See [Entity List Filters](/sdk/entity-lists/filters). ## Returns The current list of fetched entities. Appended to on each `loadMore` call. `true` while fetching the initial page or loading more. `true` if additional pages are available. Current sort field. Current reaction type used for reaction-based sorting. Current sort direction. Current sort algorithm. Change the sort field. Resets the list and re-fetches from page 1. Change the reaction type for reaction-based sorting. Resets and re-fetches. Change the sort direction. Resets the list and re-fetches from page 1. Change the sort algorithm. Resets the list and re-fetches from page 1. Load the next page of entities. Appends to the existing list. # Hooks Reference Source: https://docs.sublay.io/v7/hooks/overview Low-level hooks for every Sublay feature Browse the full reference for all Sublay hooks, organized by feature area. Each hook page documents its props, return values, and usage examples. For higher-level integration guidance, see the [SDK Reference](/sdk/getting-started). # Use user Source: https://docs.sublay.io/v7/hooks/user/use-user Access and update the authenticated user's own profile ## Overview `useUser` is the primary hook for interacting with the currently authenticated user's profile. It provides the user's full data (as an `AuthUser`), loading and error states, an `updateUser` action, and derived [suspension](/sdk/moderation/suspensions) state (`isSuspended` / `activeSuspension`). Updates are applied optimistically — the UI reflects changes immediately, and reverts automatically if the server request fails. ## Usage Example ```tsx theme={null} import { useUser } from "@sublay/react-js"; function ProfileEditor() { const { user, updating, updateUser } = useUser(); const handleSave = async () => { await updateUser({ name: "Jane Doe", bio: "Building cool things." }); }; if (!user) return null; return (

{user.name}

); } ``` ### Uploading an Avatar ```tsx theme={null} import { useUser } from "@sublay/react-js"; function AvatarUpload({ file }: { file: File }) { const { updateUser } = useUser(); const handleUpload = async () => { await updateUser({ avatar: { file, options: { width: 200, height: 200, fit: "cover" }, }, }); }; return ; } ``` ## Return Values The authenticated user's full profile. Returns `null` while loading or when no user is authenticated. See [User data model](/data-models/user). `true` while the user's profile is being fetched for the first time. `true` while an `updateUser` call is in progress. Error message if an `updateUser` call fails. `null` when there is no error. `true` while the authenticated user has an active [suspension](/sdk/moderation/suspensions). Derived from the user's `suspensions` using the same effective definition as the server. The user's effective (furthest-reaching) active suspension, or `null` when they aren't suspended. `endDate: null` means the suspension is indefinite. Bind a "suspended until X" banner or a disabled composer to this. See [Suspensions](/sdk/moderation/suspensions). Updates the authenticated user's profile. Applies optimistic updates immediately and reverts on failure. Display name. Pass `null` to clear. Unique username. Pass `null` to clear. Must be available. Short biography. Limited to 300 characters. Date of birth. Pass `null` to clear. Avatar image. Pass a URL string, a file object with upload options, or `null` to remove. Banner image. File upload only. User's geographic location. Pass `null` to clear. Arbitrary project-specific data. Limited to 10 KB. Visible to other users (embedded in entity and comment data). Private custom fields excluded from all client-facing responses, including the authenticated user's own profile. Stored server-side only — use it for data you need to persist but never expose to any client. Clears the current error state. ## Optimistic Updates `updateUser` applies changes to local state before the server responds. This makes the UI feel instant. Fields that cannot be optimistically applied (file uploads, location) wait for the server response. If the server returns an error, the hook automatically reverts the user state to its pre-update value. File uploads (avatar as a `File`/`Blob`, and banner) are not applied optimistically because the final URL is unknown until the upload completes. # Use user actions Source: https://docs.sublay.io/v7/hooks/user/use-user-actions Low-level actions for managing authenticated user state ## Overview `useUserActions` is a low-level hook that provides direct access to the Redux actions and API mutations for the current user. In most cases you should use [`useUser`](/hooks/user/use-user) instead, which wraps `useUserActions` and provides a higher-level interface. Use `useUserActions` directly when you need to manually set or clear user state — for example, in a custom authentication flow. ## Usage Example ```tsx theme={null} import { useUserActions } from "@sublay/react-js"; function CustomAuth() { const { setUser, clearUser } = useUserActions(); const handleExternalLogin = async (token: string) => { const userData = await myAuthService.login(token); setUser(userData); // Sync the authenticated user into Sublay state }; const handleLogout = () => { clearUser(); }; return ( <> ); } ``` ## Return Values Manually sets the current user in Redux state. Validates that the user object has an `id` before setting — partial or empty objects are silently ignored. Clears the current user from Redux state and resets any error. Calls the update user API and manages loading and error state. Applies optimistic updates for non-file fields. Reverts to `currentUser` if the request fails. The project ID. The authenticated user's ID. Fields to update. See [`useUser`](/hooks/user/use-user) for the full `UpdateUserParams` reference. The current user snapshot used to revert state on failure. Clears any error stored in the user slice. Prefer [`useUser`](/hooks/user/use-user) for typical profile management. `useUserActions` is intended for cases where you need direct control over user state, such as custom authentication integrations. # Check username availability Source: https://docs.sublay.io/v7/hooks/users/use-check-username-availability Check whether a username is available before setting it ## Overview `useCheckUsernameAvailability` returns a function that checks whether a given username is available in the project. Use this to validate a username in real time before calling `updateUser`. ## Usage Example ```tsx theme={null} import { useCheckUsernameAvailability } from "@sublay/react-js"; import { useState } from "react"; function UsernameInput() { const checkAvailability = useCheckUsernameAvailability(); const [available, setAvailable] = useState(null); const handleChange = async (username: string) => { if (username.length < 3) return; const result = await checkAvailability({ username }); setAvailable(result.available); }; return (
handleChange(e.target.value)} /> {available === true && Available} {available === false && Taken}
); } ``` ## Parameters The hook returns a function. That function accepts: The username to check. ## Returns `true` if the username is available, `false` if it is already taken. # Fetch user Source: https://docs.sublay.io/v7/hooks/users/use-fetch-user Fetch a user's public profile by their Sublay user ID ## Overview `useFetchUser` returns a function that fetches a user's public profile by their Sublay user ID. Use this when you have the user's `id` and need to load their profile on demand. If the profile owner has blocked the signed-in caller, this returns `404` — the same response as a user who does not exist. The reverse is not true: a user *you* have blocked stays visible, so you keep a way to find and unblock them. Requires the `moderation` bundle. ## Usage Example ```tsx theme={null} import { useFetchUser } from "@sublay/react-js"; import { useState } from "react"; function UserCard({ userId }: { userId: string }) { const fetchUser = useFetchUser(); const [user, setUser] = useState(null); const load = async () => { const result = await fetchUser({ userId }); setUser(result); }; return ; } ``` ## Parameters The hook returns a function. That function accepts: The Sublay user ID to fetch. Optional. Pass `"files"` to populate `avatarFile` and `bannerFile` with full `File` objects. ## Returns The user's public profile. When `spaceReputation` is requested, it carries an added `spaceReputation` number. See [User data model](/data-models/user) and [Reputation](/data-models/reputation). # Fetch user by foreign ID Source: https://docs.sublay.io/v7/hooks/users/use-fetch-user-by-foreign-id Fetch a user's public profile by their foreign ID ## Overview `useFetchUserByForeignId` returns a function that fetches a user's public profile using their `foreignId` — the ID from your own system that was associated with this user during authentication. Use this when you have your own user ID and need to look up the corresponding Sublay user profile. If the profile owner has blocked the signed-in caller, this returns `404` — the same response as a user who does not exist. The reverse is not true: a user *you* have blocked stays visible, so you keep a way to find and unblock them. Requires the `moderation` bundle. ## Usage Example ```tsx theme={null} import { useFetchUserByForeignId } from "@sublay/react-js"; function UserCard({ myUserId }: { myUserId: string }) { const fetchUser = useFetchUserByForeignId(); const load = async () => { const user = await fetchUser({ foreignId: myUserId }); console.log(user); }; return ; } ``` ## Parameters The hook returns a function. That function accepts: The foreign ID to look up. This is the identifier from your own system that was passed when the user was created or verified. Optional. Pass `"files"` to populate `avatarFile` and `bannerFile` with full `File` objects. ## Returns The user's public profile. When `spaceReputation` is requested, it carries an added `spaceReputation` number. See [User data model](/data-models/user) and [Reputation](/data-models/reputation). # Fetch user by username Source: https://docs.sublay.io/v7/hooks/users/use-fetch-user-by-username Fetch a user's public profile by their username ## Overview `useFetchUserByUsername` returns a function that fetches a user's public profile by their username. Use this for public profile pages where the URL contains a username slug. If the profile owner has blocked the signed-in caller, this returns `404` — the same response as a user who does not exist. The reverse is not true: a user *you* have blocked stays visible, so you keep a way to find and unblock them. Requires the `moderation` bundle. ## Usage Example ```tsx theme={null} import { useFetchUserByUsername } from "@sublay/react-js"; import { useEffect, useState } from "react"; import { User } from "@sublay/core"; function ProfilePage({ username }: { username: string }) { const fetchUser = useFetchUserByUsername(); const [user, setUser] = useState(null); useEffect(() => { fetchUser({ username }).then(setUser); }, [username]); if (!user) return null; return

{user.name}

; } ``` ## Parameters The hook returns a function. That function accepts: The username to look up. Optional. Pass `"files"` to populate `avatarFile` and `bannerFile` with full `File` objects. ## Returns The user's public profile. When `spaceReputation` is requested, it carries an added `spaceReputation` number. See [User data model](/data-models/user) and [Reputation](/data-models/reputation). # Fetch user suggestions Source: https://docs.sublay.io/v7/hooks/users/use-fetch-user-suggestions Search for users by a query string ## Overview `useFetchUserSuggestions` returns a function that searches for users by a query string. This is the underlying hook used by [`useUserMentions`](/hooks/users/use-user-mentions) for `@mention` autocomplete, but it can also be used for any user search or autocomplete UI. This hook uses an authenticated request. The user must be signed in. ## Usage Example ```tsx theme={null} import { useFetchUserSuggestions } from "@sublay/react-js"; import { useState } from "react"; function UserSearch() { const fetchSuggestions = useFetchUserSuggestions(); const [results, setResults] = useState([]); const handleSearch = async (query: string) => { const users = await fetchSuggestions({ query }); setResults(users); }; return ( handleSearch(e.target.value)} placeholder="Search users..." /> ); } ``` ## Parameters The hook returns a function. That function accepts: The search string. Matched against usernames and names. ## Returns An array of matching users' public profiles. When `spaceReputation` is requested, each carries an added `spaceReputation` number. See [User data model](/data-models/user) and [Reputation](/data-models/reputation). # Use user mentions Source: https://docs.sublay.io/v7/hooks/users/use-user-mentions Detect @mention triggers, fetch user suggestions, and track mentioned users in text input ## Overview `useUserMentions` manages the full `@mention` flow inside a text input. It monitors the cursor position and content, detects when the user is typing an `@mention`, debounces a search call, and returns suggestions to display. When the user selects a suggestion, it inserts the username into the content and tracks the mention. This hook is intended to be used alongside a controlled text input where you manage the content and cursor position. ## Usage Example ```tsx theme={null} import { useUserMentions } from "@sublay/react-js"; import { useState, useRef } from "react"; function CommentInput() { const [content, setContent] = useState(""); const [cursorPosition, setCursorPosition] = useState(0); const [isSelectionActive, setIsSelectionActive] = useState(false); const inputRef = useRef(null); const { isMentionActive, loading, mentionSuggestions, handleMentionClick, mentions, } = useUserMentions({ content, setContent, focus: () => inputRef.current?.focus(), cursorPosition, isSelectionActive, }); return (