Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.myme.so/llms.txt

Use this file to discover all available pages before exploring further.

Every API request carries a bearer token. Marfa accepts two token types, both sent in the Authorization header:
Authorization: Bearer <token>
The token prefix indicates its kind:
PrefixKind
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

Call POST /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.
POST /keys
{
  "label": "my-app",
  "role": "member",
  "source": "My App",
  "default_tier": "library",
  "type_permissions": { "core.note": "write", "core.bookmark": "read" },
  "extension_permissions": { "my-app.*": "write" },
  "edge_permissions": { "parent-of": "write", "about": "write" }
}
{
  "id": "019d...",
  "key": "marfa_k1_abc...",
  "label": "my-app",
  "role": "member",
  "source": "My App",
  "default_tier": "library",
  ...
}

Key fields

Full request and response shapes live in the Keys API reference. A few fields carry rules worth calling out:
  • roleadmin bypasses every permission check. member is 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 first POST /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

1

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.
2

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.
3

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.
4

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.)
5

Use the access token

The app calls the API with Authorization: Bearer marfa_at_.... Access tokens are short-lived (default 1 hour).
6

Refresh

Before the access token expires, the app calls POST /auth/oauth2/token with grant_type=refresh_token to get a new pair. Refresh token rotation: the old refresh token is invalidated.

Endpoint surface

EndpointSpec / purpose
POST /auth/oauth2/authorize + POST /auth/oauth2/consentAuthorization Code + PKCE S256.
POST /auth/oauth2/tokenauthorization_code, refresh_token, client_credentials. Refresh rotation built in.
POST /auth/oauth2/userinfoOIDC userinfo.
POST /auth/oauth2/revokeRFC 7009 token revocation.
POST /auth/oauth2/introspectRFC 7662 token introspection.
POST /auth/oauth2/registerRFC 7591 Dynamic Client Registration. Open to unauthenticated callers.
GET /auth/jwksJWKS for id_token signature verification.
GET /.well-known/oauth-authorization-serverRFC 8414 authorisation-server metadata.
GET /.well-known/openid-configurationOIDC discovery.
GET /auth/authorize + POST /auth/authorize/decisionMarfa-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/securityMarfa-owned user-facing grants + session management.

Scopes

Scopes use a verb-on-resource grammar that mirrors the permission maps:
ScopeMeaning
<type>:readRead items of this type (pattern matches subtypes).
<type>:writeCreate / update items of this type.
edge.<type>:readRead edges of this type.
edge.<type>:writeCreate / delete edges of this type (dual-gated with type write).
metadata:readRead tags.
metadata:writeWrite tags.
metadata.<sub>:writeWrite a specific metadata sub-resource (today: metadata.types:write gates POST /types).
Wildcards are allowed on type scopes: 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 returned scope 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 returns 400 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.
1

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.
2

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.
3

Token issuance

Once the user approves, the next poll returns the same access + refresh token pair as the Authorization Code flow. When openid is among the granted scopes, an id_token is also returned.
While the user has not yet approved, polls return 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

When openid 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 profile scope: name, preferred_username, picture
  • With email scope: email, email_verified
Verify signatures against the platform’s JWKS at /.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.
curl -X POST https://marfa.example.com/auth/oauth2/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'token=marfa_at_...'
Apps should call this on sign-out to ensure server-side state matches the user’s intent. The endpoint is advertised in the OIDC discovery doc as 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/decision and device-flow via /auth/device/consent). details.source distinguishes the surface; details.created distinguishes first-consent from re-consent.
  • auth.grant.revoked — emitted on DELETE /auth/grants/:id and the form-friendly POST /auth/grants/:id/revoke.
Both rows are written by Marfa route handlers, not by the plugin, so they carry the verified 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=S256 on 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 canonical redirect_uri values the bootstrap will accept. Requests whose redirect_uri doesn’t match (after scheme + host + path normalisation) are rejected at /oauth/start with 400. 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.
Both produce the same server behavior — permissions enforce identically regardless of credential kind.