Skip to main content

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 external BACKEND_URL or back to the same Next.js origin if no upstream is configured.
  • API keys are stored in Convex (mobileApiGatewayKeys table) as SHA-256 hashes. The gateway looks them up on each request, with a short cache controlled by MOBILE_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

VariableRequiredDefaultPurpose
NEXT_PUBLIC_CONVEX_URLyesConvex deployment URL the gateway queries for keys
INTERNAL_API_ACCESS_TOKEN or DEPLOYMENT_INGEST_TOKENyesToken the gateway uses to fetch keys from Convex AND injects into proxied requests
BACKEND_URLno(same origin)If set, proxy targets this URL instead of looping back to Next.js
MOBILE_BACKEND_API_KEYno(DB mode)Optional legacy single-key mode. Bypasses DB lookup
MOBILE_BACKEND_ALLOWED_ORIGINSno*Comma-separated CORS allow list
MOBILE_BACKEND_ALLOWED_PREFIXESno(built-in list)Override the default path allow list
MOBILE_BACKEND_TIMEOUT_MSno15000Upstream proxy timeout
MOBILE_GATEWAY_KEYS_CACHE_TTL_MSno15000DB key cache TTL

Convex deployment

VariableRequiredPurpose
INTERNAL_API_ACCESS_TOKEN or DEPLOYMENT_INGEST_TOKENyesMust match the gateway's value. Used by assertEmailActionAuthorized, getMobileApiGatewayKeysForGateway, and generateAwardCategoriesPlan
NEXT_PUBLIC_APP_URL or SITE_URLrecommendedUsed by the "Run Check" diagnostic action

Mobile client

VariableRequiredPurpose
MOBILE_API_BASE_URL or NEXT_PUBLIC_APP_URLyesThe Right Events host (e.g. https://rightevents.co)
MOBILE_API_KEYyesMobile gateway key (re_mapi_...) — store in expo-secure-store / Keychain

2.2 Generating an API key

  1. Sign in as a superadmin and visit Admin → Settings → Mobile API Gateway.
  2. Fill in: name, allowed path prefixes (leave blank for the default 8), allowed origins, optional note.
  3. Click Generate API Key. The raw key is shown once — copy it immediately. The dashboard only retains a hash.
  4. 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.

MethodPathPurpose
GET/api/mobile/backend/healthBackend readiness probe
GET/api/mobile/backend/openapiLive 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>).

DomainMethodPathAuth above gateway
PaymentsPOST/api/payments/checkoutgateway only
PaymentsPOST/api/payments/initialize-multi-currencygateway only
PaymentsGET/api/payments/status/{sessionId}gateway only
PaymentsGET/api/payments/routegateway only
OrdersGET/api/orders/{id}gateway only
WalletPOST/api/wallet/generategateway only
WalletGET/api/wallet/download/{orderId}/{passType}gateway only
BadgesPOST/api/badges/generate-zplgateway only
DPPOST/api/dp/generate-backgroundgateway only
DPPOST/api/dp/trackgateway only
DPPOST/api/dp/batch-generategateway only
DPGET/api/dp/count/{eventId}gateway only
DPGET/api/dp/campaign-stats/{shortId}gateway only
AIPOST/api/ai/analyze-imagegateway only
AIPOST/api/ai/awards/categoriesgateway-trust (Convex)
AIPOST/api/ai/copywritergateway only
AIPOST/api/ai/generate-dpgateway only
AIPOST/api/ai/insightsgateway only
AIPOST/api/ai/messaginggateway only
AIPOST/api/ai/moderationgateway only
AIPOST/api/ai/generate-backgroundgateway only
EmailPOST/api/email/sendgateway-trust (Convex)
EmailPOST/api/email/confirmationgateway-trust (Convex)
EmailPOST/api/email/team-invitegateway-trust (Convex)
EmailPOST/api/email/attendee-notificationgateway-trust (Convex)
EmailPOST/api/email/ticket-confirmationgateway only
EmailGET/api/email/trackgateway only
ModerationPOST/api/moderation/textgateway 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-token injection 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:

  1. SHA-256 hash the inbound key.
  2. Look it up in Convex via the cached getMobileApiGatewayKeysForGateway query.
  3. Verify the matched record is isActive: true.
  4. Verify the requested path matches the key's allowedPrefixes (or the global allow list if the key has none).
  5. Verify the request's Origin matches the key's allowedOrigins (or *).
  6. On success, remove the inbound x-mobile-api-key and 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 valid x-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"
}
StatusWhen
401Missing or invalid x-mobile-api-key
403Path not in this key's allowedPrefixes, or origin not allowed
404Privileged endpoint hit without internal-token / privileged session
429Gateway rate limit (180 req / 60 s per IP+namespace)
502Upstream BACKEND_URL returned a non-2xx terminal error or refused connection
504Upstream 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.

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) and x-internal-access-token was 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:

  1. 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.
  2. Network probe (diagnostic). From Convex Cloud, fetches ${NEXT_PUBLIC_APP_URL}/api/mobile/backend/api/payments/route with the key.

Result classification:

StateBadgeMeaning
HealthygreenHash matches, network probe returned 2xx
Healthy (with caveat)amberHash matches; gateway accepted the key but upstream returned 5xx, or Convex couldn't reach the gateway
FailedredHash mismatch, key deactivated, OR gateway returned 401/403 (cache stale or origin blocked)
Not TestedyellowNo 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:

  1. Privileged probes (/health, /openapi) using the internal token.
  2. Auth gates — missing key, bad key, disallowed path, CORS preflight.
  3. 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:

  1. Generates a new raw key with the same name + restrictions.
  2. Marks the old row inactive.
  3. 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_URL health 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 allowedPrefixes per app role. A read-only insights client doesn't need /api/payments/.
  • Restrict allowedOrigins for keys used in webviews. Native apps generally don't send Origin, 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

SymptomLikely causeFix
401 "Missing or invalid API key"Inbound key didn't match any active DB rowConfirm raw key, check isActive, click Run Check
401 from proxied route, with x-mobile-proxy: trueHandler requires user auth and isn't using gateway-trustWire route to use requirePrivilegedRouteUser, propagate auth.internalToken to its Convex action (§4.3)
403 "Path not allowed"Path isn't in this key's allowedPrefixesEdit the key (rotate or recreate) or use a different key
502 "Upstream backend request failed"BACKEND_URL unreachable or returning errorsCheck upstream service health
504 from gatewayUpstream took longer than MOBILE_BACKEND_TIMEOUT_MSInvestigate upstream latency; raise the timeout
Privileged probe returns 404INTERNAL_API_ACCESS_TOKEN missing / mismatchedSet the env var and redeploy
Gateway can't load DB keys (all 401)Token missing on gateway env, or Convex value differsSet both sides to the same secret
CORS error in browserOrigin not in MOBILE_BACKEND_ALLOWED_ORIGINS or key's allowedOriginsAdd origin to env or key config
  • 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 (mobileApiGatewayKeys table)
  • 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