Every API request carries a bearer token. Marfa accepts two token types, both sent in theDocumentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
Authorization header:
| Prefix | Kind |
|---|---|
marfa_k1_... | API key |
marfa_at_... | OAuth access token |
marfa_rt_... | OAuth refresh token (used only at the refresh endpoint) |
API keys
Bearer tokens, suitable for development, automation, server-to-server calls, and app-to-Marfa in self-hosted deployments.Creating keys
CallPOST /keys with the label, role, and the three permission maps (types, extensions, edges). The response contains the plaintext key once — the server never shows it again. Store it securely. Rotating means revoking the old key and creating a new one.
Example request and response
Example request and response
Key fields
Full request and response shapes live in the Keys API reference. A few fields carry rules worth calling out:role—adminbypasses every permission check.memberis scoped by the three permission maps.source— stamped onto every item written by this key. Unique per tenant; clients can’t override.default_tier— stamps onto items when the client omits the field. Clients can override per item. See Tier.type_permissions,extension_permissions,edge_permissions— see Permissions for resolution rules and the dual-gate.
Bootstrap
On a fresh server with no keys, the firstPOST /keys succeeds without auth (bootstrap mode). Once any key exists, bootstrap mode disables — further key creation requires an admin token.
If all keys are later revoked, bootstrap mode does not re-enable automatically. Recovery requires server-side admin intervention (dropping and recreating the keys table, typically).
Managing keys
List, retrieve, update, and revoke via the Keys endpoints. Admin-scoped — non-admin keys cannot manage other keys.Revocation
DELETE /keys/{id} revokes immediately. Revoked tokens return 401 unauthorized on the next request.
OAuth 2.1
Authorization Code flow with PKCE. The “Sign in with Marfa” flow for third-party apps. The OAuth-protocol surface is owned by the@better-auth/oauth-provider plugin mounted on Marfa’s Better Auth instance. Endpoints land under /auth/oauth2/*; discovery + JWKS are re-published at the standard well-known paths. The consent screen and grants management remain Marfa-owned and sit on top of the plugin’s tables.
High-level flow
Client registration
An app registers as an OAuth client. Two paths: dynamic registration via
POST /auth/oauth2/register (RFC 7591, open — allowUnauthenticatedClientRegistration: true), or admin-side via POST /auth/clients. Public clients (native apps, SPAs) use PKCE without a client secret; confidential clients (server apps) receive a client_secret.Authorization request
The app redirects the user to
GET /auth/authorize with client_id, redirect_uri, scope, state, code_challenge, and code_challenge_method=S256. If the user has no active session, the server redirects to /auth/sign-in (a server-rendered HTML form) preserving the original authorize URL as a return target. After sign-in, the user lands on the consent screen listing exactly which types, extensions, and edges the app is requesting.Consent
The user approves (or modifies) the scope. The consent form POSTs to
/auth/authorize/decision, which forwards to the plugin’s /auth/oauth2/consent and redirects back to redirect_uri with an authorization code.Token exchange
The app exchanges the code at
POST /auth/oauth2/token with the code_verifier. Response: access token, refresh token, scope granted. (POST /auth/token is preserved as a back-compat alias.)Use the access token
The app calls the API with
Authorization: Bearer marfa_at_.... Access tokens are short-lived (default 1 hour).Endpoint surface
| Endpoint | Spec / purpose |
|---|---|
POST /auth/oauth2/authorize + POST /auth/oauth2/consent | Authorization Code + PKCE S256. |
POST /auth/oauth2/token | authorization_code, refresh_token, client_credentials. Refresh rotation built in. |
POST /auth/oauth2/userinfo | OIDC userinfo. |
POST /auth/oauth2/revoke | RFC 7009 token revocation. |
POST /auth/oauth2/introspect | RFC 7662 token introspection. |
POST /auth/oauth2/register | RFC 7591 Dynamic Client Registration. Open to unauthenticated callers. |
GET /auth/jwks | JWKS for id_token signature verification. |
GET /.well-known/oauth-authorization-server | RFC 8414 authorisation-server metadata. |
GET /.well-known/openid-configuration | OIDC discovery. |
GET /auth/authorize + POST /auth/authorize/decision | Marfa-owned consent screen + decision handler (forwards to the plugin’s /auth/oauth2/consent). |
/auth/device* | Marfa-owned device-flow state machine; tokens land in the plugin’s tables on issuance. |
/auth/grants*, /auth/sessions*, /auth/security | Marfa-owned user-facing grants + session management. |
Scopes
Scopes use a verb-on-resource grammar that mirrors the permission maps:| Scope | Meaning |
|---|---|
<type>:read | Read items of this type (pattern matches subtypes). |
<type>:write | Create / update items of this type. |
edge.<type>:read | Read edges of this type. |
edge.<type>:write | Create / delete edges of this type (dual-gated with type write). |
metadata:read | Read tags. |
metadata:write | Write tags. |
metadata.<sub>:write | Write a specific metadata sub-resource (today: metadata.types:write gates POST /types). |
core.bookmark.*:read covers bookmark and all its subtypes; *:read covers everything.
Extension namespaces are not part of the OAuth scope grammar — extension reads and writes are governed by the credential’s extension_permissions map (see Permissions). OAuth tokens issued by the consent flow inherit a default extension grant from the requesting client’s registration.
The consent UI renders the resolved type patterns literally — users see exactly what they’re granting.
Partial grants
Users can approve a subset of requested scopes. The returnedscope parameter in the token response lists what was actually granted. Apps must handle receiving less than they asked for.
Refresh token rotation
Each refresh invalidates the prior refresh token and issues a new one. Reusing an invalidated refresh token returns400 token_reuse_detected and signals that the token pair has been compromised — both access and refresh tokens are invalidated. The app must restart the authorization flow.
Device Authorization Grant
For headless and CLI contexts where the Authorization Code flow’s browser-redirect can’t run, the platform implements RFC 8628 Device Authorization Grant.Initiate
The CLI calls
POST /auth/device with client_id and scope. Response carries device_code, a short human-readable user_code, the verification_uri to open in a browser, the verification_uri_complete form with the user code embedded as a query parameter (so the form pre-fills), and the interval between polls.Display and poll
The CLI prints the URL and code; the user opens the URL on any browser-capable device, signs in if needed, enters or confirms the code, and approves. Meanwhile the CLI polls
POST /auth/device/token at the declared interval. Visiting GET /auth/device?user_code=XXX-XXXX short-circuits straight to the consent screen.400 authorization_pending. Polling faster than the declared interval returns 400 slow_down and bumps the interval up. Past the device-code expiry, polls return 400 expired_token.
Sign-in and sign-up surfaces
The platform serves server-rendered HTML at/auth/sign-in and (when sign-up is enabled) /auth/sign-up. These are the surfaces the consent flow redirects unauthenticated users to. They also stand alone for direct sign-in to the platform’s own console.
OIDC identity tokens
Whenopenid is among the granted scopes, the token endpoint returns an id_token alongside the access and refresh tokens. The id token is a signed JWT carrying:
- Required claims:
iss,sub,aud,exp,iat - With
profilescope:name,preferred_username,picture - With
emailscope:email,email_verified
/.well-known/jwks.json. Algorithms supported are advertised in the OIDC discovery doc at /.well-known/oauth-authorization-server under id_token_signing_alg_values_supported (today: RS256).
For most clients the userinfo endpoint at /auth/userinfo is the simpler way to fetch claim data — it returns the same shape over a request rather than as a JWT. Use the id token when verifying identity statelessly without a server round-trip; use userinfo when you have a bearer in hand and want the current claim values.
Token revocation
POST /auth/oauth2/revoke implements RFC 7009. Pass the access or refresh token as token in a form-encoded body. The server invalidates the token (and, for refresh tokens, the entire grant) and returns 200 regardless of whether the token was valid — per the spec, to avoid leaking token-existence information.
revocation_endpoint.
Token introspection
POST /auth/oauth2/introspect implements RFC 7662. Form-encoded body with token and (for confidential clients) client_id + client_secret. The server returns { active: boolean, scope?, client_id?, exp?, ... } — active: false is the not-valid response, never an error.
Grant lifecycle audit
The auth audit-row catalog carries two grant-lifecycle entries:auth.grant.created— emitted on consent acceptance (both code-flow via/auth/authorize/decisionand device-flow via/auth/device/consent).details.sourcedistinguishes the surface;details.createddistinguishes first-consent from re-consent.auth.grant.revoked— emitted onDELETE /auth/grants/:idand the form-friendlyPOST /auth/grants/:id/revoke.
client_id, user_id, and grant item id without trusting form fields.
Grants management
/auth/grants exposes the user’s current OAuth grants — one row per system.connection of kind: app. Listing, inspecting, and revoking grants happens through this surface. Revoking a grant invalidates the associated tokens immediately. See Connections for the underlying type.
Connector OAuth bootstrap
Distinct from the user-facing OAuth flow above: the platform also runs an OAuth callback at/oauth/callback/:provider for Integration connectors that need to establish OAuth tokens against an external service (Google Calendar, GitHub, etc.). The callback verifies a server-signed state, exchanges the upstream code for tokens, and stores them encrypted for the connector’s later use through the OAuth proxy. End users see a normal upstream OAuth flow; the platform handles the bootstrap on the connector’s behalf. See Integrations for the wider context.
The bootstrap flow is hardened with two platform-side controls:
- PKCE on the upstream authorize call. The platform generates a code verifier per attempt, persists it inside the AES-GCM-signed state envelope, and sends
code_challenge+code_challenge_method=S256on the authorize redirect. The verifier is replayed at token exchange. Connectors don’t manage PKCE themselves — it’s enforced by the platform regardless of what the upstream service expects, defending against authorization-code injection. - Redirect allow-list.
MARFA_OAUTH_REDIRECT_ALLOWLIST(comma-separated) lists the canonicalredirect_urivalues the bootstrap will accept. Requests whoseredirect_uridoesn’t match (after scheme + host + path normalisation) are rejected at/oauth/startwith400. Required in production; misconfigured deployments fail loudly rather than accept arbitrary redirects.
Choosing between API keys and OAuth
- API keys for: your own apps against your own tenant, server-to-server calls, scripts, CI, MCP servers and CLIs connecting to a known tenant.
- OAuth for: third-party apps where the user wants explicit consent per type, revocable without rotating an API key, and a flow that terminates at a login experience.