Mobile API Gateway
Right Events exposes a controlled mobile integration surface at /api/mobile/backend/*. This page is the canonical reference for any team integrating against it.
1. Architecture
mobile client ──x-mobile-api-key──▶ Next.js gateway ──x-internal-access-token──▶ underlying handler
/api/mobile/backend/... (Next.js route + Convex action)
- The gateway lives at
app/api/mobile/backend/[...slug]/route.ts. It validates the mobile API key, enforces path-prefix and origin restrictions, then proxies the request — either to an externalBACKEND_URLor back to the same Next.js origin if no upstream is configured. - API keys are stored in Convex (
mobileApiGatewayKeystable) as SHA-256 hashes. The gateway looks them up on each request, with a short cache controlled byMOBILE_GATEWAY_KEYS_CACHE_TTL_MS(default 15 s). - After successful validation, the gateway injects four headers into the proxied request:
x-internal-access-token,x-mobile-gateway-key-id,x-mobile-gateway-key-name,x-mobile-gateway-key-prefix. Inbound copies of these headers are stripped, so callers cannot forge them.
2. Setup
2.1 Environment variables
Next.js gateway
| Variable | Required | Default | Purpose |
|---|---|---|---|
NEXT_PUBLIC_CONVEX_URL | yes | — | Convex deployment URL the gateway queries for keys |
INTERNAL_API_ACCESS_TOKEN or DEPLOYMENT_INGEST_TOKEN | yes | — | Token the gateway uses to fetch keys from Convex AND injects into proxied requests |
BACKEND_URL | no | (same origin) | If set, proxy targets this URL instead of looping back to Next.js |
MOBILE_BACKEND_API_KEY | no | (DB mode) | Optional legacy single-key mode. Bypasses DB lookup |
MOBILE_BACKEND_ALLOWED_ORIGINS | no | * | Comma-separated CORS allow list |
MOBILE_BACKEND_ALLOWED_PREFIXES | no | (built-in list) | Override the default path allow list |
MOBILE_BACKEND_TIMEOUT_MS | no | 15000 | Upstream proxy timeout |
MOBILE_GATEWAY_KEYS_CACHE_TTL_MS | no | 15000 | DB key cache TTL |
Convex deployment
| Variable | Required | Purpose |
|---|---|---|
INTERNAL_API_ACCESS_TOKEN or DEPLOYMENT_INGEST_TOKEN | yes | Must match the gateway's value. Used by assertEmailActionAuthorized, getMobileApiGatewayKeysForGateway, and generateAwardCategoriesPlan |
NEXT_PUBLIC_APP_URL or SITE_URL | recommended | Used by the "Run Check" diagnostic action |
Mobile client
| Variable | Required | Purpose |
|---|---|---|
MOBILE_API_BASE_URL or NEXT_PUBLIC_APP_URL | yes | The Right Events host (e.g. https://rightevents.co) |
MOBILE_API_KEY | yes | Mobile gateway key (re_mapi_...) — store in expo-secure-store / Keychain |
2.2 Generating an API key
- Sign in as a superadmin and visit Admin → Settings → Mobile API Gateway.
- Fill in: name, allowed path prefixes (leave blank for the default 8), allowed origins, optional note.
- Click Generate API Key. The raw key is shown once — copy it immediately. The dashboard only retains a hash.
- Paste the key back into the row's test field and click Run Check to verify.
3. Endpoint catalog
23 endpoints in total: 2 privileged probes plus 21 proxied routes. The canonical list lives in lib/mobileApiGatewayConfig.ts (MOBILE_GATEWAY_ROUTE_SPECS).
3.1 Privileged probes
These require x-internal-access-token matching INTERNAL_API_ACCESS_TOKEN, or a logged-in privileged user session. They return 404 to anyone else, so they're not safe to expose to mobile clients in production.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/mobile/backend/health | Backend readiness probe |
| GET | /api/mobile/backend/openapi | Live route manifest + reachability |
3.2 Proxied routes
All paths are prefixed with /api/mobile/backend. Requests must include x-mobile-api-key: <key> (or Authorization: Bearer <key>).
| Domain | Method | Path | Auth above gateway |
|---|---|---|---|
| Payments | POST | /api/payments/checkout | gateway only |
| Payments | POST | /api/payments/initialize-multi-currency | gateway only |
| Payments | GET | /api/payments/status/{sessionId} | gateway only |
| Payments | GET | /api/payments/route | gateway only |
| Orders | GET | /api/orders/{id} | gateway only |
| Wallet | POST | /api/wallet/generate | gateway only |
| Wallet | GET | /api/wallet/download/{orderId}/{passType} | gateway only |
| Badges | POST | /api/badges/generate-zpl | gateway only |
| DP | POST | /api/dp/generate-background | gateway only |
| DP | POST | /api/dp/track | gateway only |
| DP | POST | /api/dp/batch-generate | gateway only |
| DP | GET | /api/dp/count/{eventId} | gateway only |
| DP | GET | /api/dp/campaign-stats/{shortId} | gateway only |
| AI | POST | /api/ai/analyze-image | gateway only |
| AI | POST | /api/ai/awards/categories | gateway-trust (Convex) |
| AI | POST | /api/ai/copywriter | gateway only |
| AI | POST | /api/ai/generate-dp | gateway only |
| AI | POST | /api/ai/insights | gateway only |
| AI | POST | /api/ai/messaging | gateway only |
| AI | POST | /api/ai/moderation | gateway only |
| AI | POST | /api/ai/generate-background | gateway only |
| POST | /api/email/send | gateway-trust (Convex) | |
| POST | /api/email/confirmation | gateway-trust (Convex) | |
| POST | /api/email/team-invite | gateway-trust (Convex) | |
| POST | /api/email/attendee-notification | gateway-trust (Convex) | |
| POST | /api/email/ticket-confirmation | gateway only | |
| GET | /api/email/track | gateway only | |
| Moderation | POST | /api/moderation/text | gateway only |
| Addons | * | /api/addon/{slug...} | gateway only |
Gateway-trust routes wrap a privileged Next.js handler that previously required an organizer session. They now accept the gateway's
x-internal-access-tokeninjection as authority — see §4.3.
4. Authentication model
4.1 Mobile API key (gateway layer)
Every proxied request must include the key as either x-mobile-api-key: re_mapi_... or Authorization: Bearer re_mapi_....
Validation steps:
- SHA-256 hash the inbound key.
- Look it up in Convex via the cached
getMobileApiGatewayKeysForGatewayquery. - Verify the matched record is
isActive: true. - Verify the requested path matches the key's
allowedPrefixes(or the global allow list if the key has none). - Verify the request's
Originmatches the key'sallowedOrigins(or*). - On success, remove the inbound
x-mobile-api-keyand proxy.
4.2 Internal access token (gateway → handler)
After validation, the gateway injects:
x-internal-access-token: <INTERNAL_API_ACCESS_TOKEN>
x-mobile-gateway-key-id: <Convex key id>
x-mobile-gateway-key-name: <human-readable name>
x-mobile-gateway-key-prefix: <first 8 chars of key>
These four headers are stripped from any inbound request via EXCLUDED_REQUEST_HEADERS, so the only way they can reach a downstream handler is through the gateway after a successful key validation.
4.3 Gateway-trust pattern
requirePrivilegedRouteUser (in lib/serverSecurity.ts) returns a discriminated union:
| { ok: true; kind: "user"; token: string; user: User }
| { ok: true; kind: "gateway"; internalToken: string; keyId?: string; keyName?: string }
| { ok: false; response: NextResponse }
kind: "user"— a real Convex user session is present, with organizer/admin/superadmin privileges.kind: "gateway"— the request carries a validx-internal-access-token. The mobile gateway has already authenticated the caller; treat them as a trusted service identity.
Routes that delegate to a Convex action propagate internalToken when sourced from the gateway:
const auth = await requirePrivilegedRouteUser(request);
if (!auth.ok) return auth.response;
const result = await fetchAction(
api.emails.sendEmail,
{
to,
subject,
html,
...(auth.kind === "gateway" ? { internalToken: auth.internalToken } : {}),
},
auth.kind === "user" ? { token: auth.token } : undefined,
);
The Convex action accepts either a real authenticated identity or the matching internalToken:
async function assertEmailActionAuthorized(ctx, internalToken) {
const identity = await ctx.auth.getUserIdentity();
const hasInternalToken = !!INTERNAL_EMAIL_TOKEN && internalToken === INTERNAL_EMAIL_TOKEN;
if (!identity && !hasInternalToken) throw new Error("Unauthorized");
}
This pattern is wired today for email/send, email/confirmation, email/team-invite, email/attendee-notification, and ai/awards/categories. New routes that need it should follow the same shape.
4.4 Legacy single-key mode
Setting MOBILE_BACKEND_API_KEY in the gateway env enables a single static key that bypasses the DB lookup. Useful for local development; not recommended for production.
5. Error contract
All gateway-level errors share this shape:
{
"success": false,
"error": "Unauthorized",
"detail": "Missing or invalid API key. Use an active mobile gateway key or set MOBILE_BACKEND_API_KEY.",
"requested": "/api/payments/checkout"
}
| Status | When |
|---|---|
401 | Missing or invalid x-mobile-api-key |
403 | Path not in this key's allowedPrefixes, or origin not allowed |
404 | Privileged endpoint hit without internal-token / privileged session |
429 | Gateway rate limit (180 req / 60 s per IP+namespace) |
502 | Upstream BACKEND_URL returned a non-2xx terminal error or refused connection |
504 | Upstream timed out (MOBILE_BACKEND_TIMEOUT_MS) |
Domain-level handler errors (validation, "not found") flow through unchanged as 4xx with the handler's own JSON body.
Diagnostic headers on every proxied response: x-mobile-proxy: true, x-proxy-target, x-request-id.
6. Testing the API
There are three ways to test the gateway, ranked by progression.
6.1 In-dashboard tester (recommended for ad-hoc)
Admin → Settings → Mobile API Gateway → API Tester lets you fire any documented endpoint against the current deployment with any active key. Pick an endpoint, edit the request body, click Send, see status / latency / response headers / body inline. No external tooling required.
For each request, the tester reports:
- HTTP status with PASS / WARN / FAIL classification
- Latency in milliseconds
- Whether the response carried
x-mobile-proxy: true(proxy worked) andx-internal-access-tokenwas successfully injected - The full JSON body (or raw text)
6.2 Per-key Run Check (key health monitor)
The same admin page has a per-key Run Check that does a two-layer test:
- DB hash check (primary). Hashes the pasted key and compares to the stored hash. This is authoritative — a hash match means real production traffic with that key would be accepted.
- Network probe (diagnostic). From Convex Cloud, fetches
${NEXT_PUBLIC_APP_URL}/api/mobile/backend/api/payments/routewith the key.
Result classification:
| State | Badge | Meaning |
|---|---|---|
| Healthy | green | Hash matches, network probe returned 2xx |
| Healthy (with caveat) | amber | Hash matches; gateway accepted the key but upstream returned 5xx, or Convex couldn't reach the gateway |
| Failed | red | Hash mismatch, key deactivated, OR gateway returned 401/403 (cache stale or origin blocked) |
| Not Tested | yellow | No check has run yet |
6.3 CLI smoke harness (CI / scripted)
scripts/test-mobile-api.mjs runs end-to-end smoke tests:
MOBILE_API_KEY=re_mapi_… \
INTERNAL_ACCESS_TOKEN=… \
BASE_URL=http://localhost:3000 \
node scripts/test-mobile-api.mjs
It runs:
- Privileged probes (
/health,/openapi) using the internal token. - Auth gates — missing key, bad key, disallowed path, CORS preflight.
- Reachability probes for every documented route. Each result is classified
PASS/AUTH-REQ/BACKEND-DOWN/HANDLER-ERR/FAIL.
Exit code is non-zero only on FAIL. Wire this into CI for any deploy that touches app/api/mobile/, lib/serverSecurity.ts, or proxied route handlers.
7. Mobile SDK
The typed SDK lives at mobile/api/RightEventsApiClient.ts. Initialise once at app startup:
import {
RightEventsApiClient,
buildMobileClientConfigFromEnv,
} from "@/mobile/api/RightEventsApiClient";
const cfg = buildMobileClientConfigFromEnv("MOBILE");
if (!cfg) throw new Error("MOBILE_API_BASE_URL or MOBILE_API_KEY missing");
export const rightEventsApi = new RightEventsApiClient({
baseUrl: cfg.baseUrl,
apiKey: cfg.apiKey,
timeoutMs: 30000,
retries: 1,
defaultHeaders: { "x-mobile-platform": "react-native" },
debug: __DEV__,
});
The SDK accepts the base URL with or without /api/mobile/backend — it normalises automatically. All non-2xx responses throw RightEventsApiError with status, payload, path, method. 408 / 429 / 500 / 502 / 503 / 504 auto-retry up to retries times.
Method coverage: getMobileHealth, getMobileOpenApi, checkout, initializeMultiCurrencyCheckout, getPaymentStatus, getOrder, generateDpBackground, generateAiBackground, batchGenerateDp, trackDpCreation, getEventDpCount, moderateText, sendTemplatedEmail, sendTicketConfirmationEmail, generateBadgeZpl, getBadgeTemplates, generateWalletPass, downloadWalletPass, addOnRequest, request, requestBlob.
For full per-method signatures and example bootstrap patterns, see mobile/README.md.
8. Operations
8.1 Key rotation
Click Rotate on a key row. The dashboard:
- Generates a new raw key with the same name + restrictions.
- Marks the old row inactive.
- Returns the new raw value once.
Plan a brief overlap when both keys are active so mobile clients can swap without downtime.
8.2 Monitoring signals
Alert on:
- Spike in 401 rate on
/api/mobile/backend/*— suggests a rotated key wasn't deployed to clients. - Spike in 403 rate — clients hitting unallowed paths.
- 502 / 504 rate — upstream
BACKEND_URLhealth issue. lastCheckStatus = "warning"or"failed"on any active key in/admin/settings.
8.3 Security baseline
- Keys are secrets. Store in
expo-secure-store/ Keychain / Keystore. Never log raw keys. - Rotate aggressively on suspected exposure.
- Restrict
allowedPrefixesper app role. A read-only insights client doesn't need/api/payments/. - Restrict
allowedOriginsfor keys used in webviews. Native apps generally don't sendOrigin, so*is fine. - Rate limits. 180 req/min per IP per namespace. Bursts above this return 429.
- Internal token rotation requires coordinated update of three places: Next.js gateway env, Convex deployment env, and any external services that talk to internal endpoints.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 "Missing or invalid API key" | Inbound key didn't match any active DB row | Confirm raw key, check isActive, click Run Check |
401 from proxied route, with x-mobile-proxy: true | Handler requires user auth and isn't using gateway-trust | Wire route to use requirePrivilegedRouteUser, propagate auth.internalToken to its Convex action (§4.3) |
| 403 "Path not allowed" | Path isn't in this key's allowedPrefixes | Edit the key (rotate or recreate) or use a different key |
| 502 "Upstream backend request failed" | BACKEND_URL unreachable or returning errors | Check upstream service health |
| 504 from gateway | Upstream took longer than MOBILE_BACKEND_TIMEOUT_MS | Investigate upstream latency; raise the timeout |
| Privileged probe returns 404 | INTERNAL_API_ACCESS_TOKEN missing / mismatched | Set the env var and redeploy |
| Gateway can't load DB keys (all 401) | Token missing on gateway env, or Convex value differs | Set both sides to the same secret |
| CORS error in browser | Origin not in MOBILE_BACKEND_ALLOWED_ORIGINS or key's allowedOrigins | Add origin to env or key config |
10. Related code
- Gateway proxy:
app/api/mobile/backend/[...slug]/route.ts - Privileged probes:
app/api/mobile/backend/health/route.ts,openapi/route.ts - Static config + manifest:
lib/mobileApiGatewayConfig.ts - Auth helper (gateway-trust pattern):
lib/serverSecurity.ts - DB schema:
convex/schema.ts(mobileApiGatewayKeystable) - Admin queries / mutations / actions:
convex/settings.ts - Admin UI:
components/admin/MobileApiGatewayManager.tsx,components/admin/MobileApiTester.tsx - Mobile SDK:
mobile/api/RightEventsApiClient.ts - Smoke harness:
scripts/test-mobile-api.mjs - Reference doc:
mobile/README.md