Authentication
Two layers - an API key that identifies your app, and a user session that identifies the person acting. How they compose on a request.
Rising Tide Protocol has two authentication layers, and two authorization gates behind them:
- API key authentication - identifies your app to the protocol API
- User identity - identifies the person acting, via a hosted account (Google or X, with a wallet made for them) or BYOW (Bring Your Own Wallet)
- Entitlements - what that person may do, anywhere on the protocol
- App access - whether they may act in this particular app
In this section
- API Keys - What a key carries, scope, rotation
- User Identity - Privy, BYOW, and the user identity model
- RBAC - Role-Based Access Control, the five roles, entitlement verbs, who can mint invites
- Provider Setup - Configuring Privy for a new app
Layer 1: your API key
Every request from an app carries an API key in the X-API-Key header. The key:
- Identifies which app is making the request
- Carries an optional app binding - a bound key may only generate contracts for its own app
- Carries a read-only or read/write flag
An invalid or missing key is a 401. A handful of routes that act as a person verify the session token alone and do not check the key (the API reference marks them user), but send the key on every request anyway: the SDK does, and the app a request belongs to is read from it.
Layer 2: the user session
Operations that act on behalf of a person - minting, transferring, managing invites - also carry that user's session token in the Authorization: Bearer header. That token is either a Privy access token or a signed Stacks message from a bring-your-own-wallet session. The API tells them apart by shape: a value starting stx: is a wallet token, in the form stx:<address>:<timestamp>:<publicKey>:<signature> with the signature over RT Auth:<address>:<timestamp>, and a dotted JWT is Privy's. A wallet token is valid for a window either side of its timestamp (30 minutes by default) and is not refreshed for you, so a stale one is a 401: sign again and retry once. The API verifies the token and resolves the user's Stacks address and roles.
How they compose
Public reads - browsing an events calendar without signing in - flow through with app-only context. User-acting routes carry both, and pass the entitlement and app access checks before any state change.
Admin-only operations authorize on the person, not the key: the session token must resolve to a user whose role holds the endpoint's entitlement (every operator entitlement is held by the Admin role). There is no admin-scoped key.
Entitlements and app access
The two gates behind the session answer different questions, and a create or collect route needs both.
- An entitlement is about the person: what they may do anywhere on the protocol, such as
CREATE_PAGES. A missing one is a403with codeENTITLEMENT_REQUIRED, anderror.details.entitlementnames it, so a client can say "you need creator access" instead of a bare refusal. See RBAC. - App access is about the place: whether the person may act in the app the API key belongs to. An app is either
PUBLIC(no grant needed) orGRANTED(an active access grant is needed). A missing grant is a403with codeAPP_ACCESS_REQUIRED, anderror.detailscarries theappIdandappName. An invite, an achievement or an operator grants access; an operator can revoke it.
Access gates acquisition and participation: publishing, deploying, submitting, reserving, and creating drafts. It does not gate what a person already owns. Burning, transferring and detaching an edition are checked against ownership only, because ownership never expires.
GET /api/apps/{id}/access answers hasAccess for an app and the current session without attempting a write, which is the read an entry screen asks before it draws. Read it, rather than the app catalog, to decide whether to show a locked state: the catalog lists a teaser only for apps that choose to show one, so absence from it says nothing about permission.
Invites and sign-up
Signup is POST /api/users, and the session token must prove the address in the body. Where an app requires an invite (requireInviteForSignup on GET /api/apps/{id}/config), a new user without a code is refused with 403 INVITE_REQUIRED. The code rides in the X-Invite-Code header or the body's inviteCode, and it is claimed in the same transaction that creates the user, so an invalid code creates no account. An existing account claims with POST /api/invites/claim.
An invite can be locked to a recipient. An email lock is checked against the email Privy has verified for the signed-in person (a Google account counts), an X lock against their verified X handle, and a wallet lock against their address. Neither the person who minted an invite nor the person holding it to send can claim it. See Invites and the invite routes.
Rate limits
Requests are limited per IP across the whole API, and per user or per IP on the routes that cost something: signing, the faucets, wallet creation, contract generation, invite preview and claim, and the waitlist. A limited request is 429 RATE_LIMITED with Retry-After and RateLimit-* headers; see the API reference.
Checking entitlements in your app
import { useProtocolUser } from "@rising-tide/ui/auth";
function CreatePageButton() {
const { hasEntitlement } = useProtocolUser();
if (!hasEntitlement("CREATE_PAGES")) return <LockedState />;
return <Button>Create Page</Button>;
}See RBAC for the roles and Entitlements for the full verb catalog.