PostgREST HTTP
Most apps should use the Data API / MCP (discover → describe → read / create / update / delete / call). That surface never speaks SQL and never asks you to hold a database secret.
This page is the other door: standard PostgREST HTTP. It exists so an existing PostgREST client (or a binary you already run) can talk to App Data with config only. You still never receive a Postgres DSN. Isolation is still RLS on the serving database.
jwt-secret follows the JWKS sidecar.When to use which
| You want… | Use |
|---|---|
| An agent / MCP client that must not invent SQL | Data API at https://appdata-mcp.orkestia.dev |
A PostgREST client you already have (POSTGREST_URL + JWKS) | This page |
Do not mint a 120-second HS256 yourself. Do not invent tenant_id. Do not send a Cognito / org-member token as if it were an end-user. A 401 or 403 from /rest/v1 fails closed — there is no privileged fallback that retries as appdata_owner_*.
URL and JWKS
POSTGREST_URL=https://appdata.orkestia.dev/rest/v1
Verify the end-user JWT against the public JWKS (RS256, kid prod-1 today):
https://workflow-api.orkestia.dev/api/auth/end-user/jwks
The same key set is republished at https://appdata.orkestia.dev/.well-known/jwks.json. https://api.orkestia.dev/api/auth/end-user/jwks is 404 — that host is api-core, not the IdP. https://login.orkestia.dev/.well-known/jwks.json currently serves the SPA HTML; use the workflow-api URL until that host publishes keys.
Issuer is https://login.orkestia.dev. Do not set PGRST_JWT_AUD on a client that shares one PostgREST with every app: aud is the app's client_key.
Catalog for v1 is PostgREST OpenAPI (follow-privileges). There is no shared mcp_catalog schema on the serving database. Set the schema to app_<hex> (see claims), not public.
Token
Sign-in is the existing PKCE / /token flow (@orkestia/auth or your own). Access TTL is one hour with a rotating refresh token.
The access token keeps the IdP fields (iss, sub, aud, org, app, user_type, email, iat, exp, jti) and adds:
| Claim | Value |
|---|---|
role | appdata_r_ + UUID hex of the identity app (no dashes) |
app_schema | app_ + the same hex |
end_user_uuid | that app's EndUser.uuid (not an alias of sub forever) |
identity_app_uuid | same as app |
app_organization_uuid | optional in-app workspace. Omit the key when unbound. Never null. |
org is the paid platform organization that owns the Identity App. It is not the workspace. Bind a workspace by sending app_organization_uuid on POST /api/auth/end-user/token (authorization-code and refresh). The server checks membership and 403s workspace_forbidden if the user is not in that workspace. Do not implicit-bind when the user has exactly one workspace. Never put appdata_owner_* or tenant_id on an end-user token.
Owner-mode tables filter on end_user_uuid from request.jwt.claims. A token that only has IdP sub / app fails closed.
Accept-Profile
Send Accept-Profile: app_<hex> (and Content-Profile on writes) matching app_schema. A mismatch is refused.
Workspace header (Data API only)
The MCP / Data API gateway still accepts X-App-Organization-Uuid / X-Orkestia-Workspace for Path A. Path B binds the workspace in the token so a process that only holds the JWT does not need that header.
