The Stryde Platform
One platform that is the UI container and control plane for many apps — it mounts their interfaces, secures them, observes them, configures them, and runs their whole lifecycle.
What Stryde is
An app on the platform is three things — a frontend the shell mounts, one or more backend microservices, and a manifest that declares how both plug in. The platform gives every app shared, governed capabilities, so none of them re-implement the basics.
UI container
One Expo (React Native + Web) shell mounts every app's micro-frontend. Web and native from one codebase.
Control plane
A FastAPI service is the registry, RBAC, lookups, i18n, error codes, and app-lifecycle brain.
Governed by default
Auth, telemetry, theming, i18n, lookups, errors and inter-app comms come from the SDK — not each app.
Single-tenant by design: multi-tenancy is a separate deployment of the whole platform, not tenant-scoped data inside one instance.
How the pieces fit
The device runs the shell + SDK. Every backend call goes through the gateway, which load-balances across replicas and hands off to the control plane to mint an internal token. App backends trust only that token. Everything emits OpenTelemetry.
Shell + SDK
Owns navigation, routing, app mounting, and the SDK runtime (auth, telemetry, theme, i18n, lookups, bus, errors).
Gateway
Traefik load-balances across replicas; forward-auth swaps the Clerk session for an internal token injected upstream.
Observability
OTel traces + metrics + logs → SigNoz, with user_id / session_id / app_id on every span.
What's built today
The control plane and the deploy gate run and are tested. Everything else lives in Coming Soon.
| Capability | What it does | Status |
|---|---|---|
| App registry & lifecycle | register → validate → deploy → enable/disable → rollback | live |
| Validation gate | mechanically enforces the app standards on every manifest | live |
| Lookups (type system) | statuses/types as data, not enums; namespaced & cached | live |
| i18n (locale-maps) | EN + AR, RTL-ready, N-language storage, fallback chain | live |
| Error codes | backends emit codes; platform localizes them | live |
| RBAC model | org → team → user, roles, permissions, assignments | live |
| Shell + mounting | fetches enabled-apps feed, mounts micro-frontends | scaffolded |
| Auth (Clerk + internal token) | verify session, mint internal token | coming |
An app, in three parts
Each part deploys to a different place. The frontend ships to the device; the backend runs server-side behind the gateway; the manifest wires both and is the contract the platform validates.
Instance invisibility
Scaling a backend changes nothing the frontend can see — it always calls /api/apps/{app_key}; Traefik handles the replicas.
Inter-app communication
Apps emit/consume namespaced, versioned events through the SDK bus, and deep-link into each other via navigateToApp().
One inbox for every app
The Notification Center is a platform capability, shipped in the SDK. Any app emits a notification through the platform with one call; the shell renders it for the recipient — a bell + drawer on every surface, and a full-screen page to manage them all. Informational messages are read/acknowledged; actionable ones capture a structured response (approve/reject, yes/no, single/multiple/custom choice, free text, file upload, comment, or a composite of these), plus comments and a full audit timeline.
The bell
In the shell's top nav on every surface, with a live unread badge. Mounted automatically — it's shell chrome, so apps never build their own. Quick triage.
The drawer
Click the bell for a glass drawer: grouped inbox, filter tabs (All / Unread / Action needed), detail view, and the response controls — without leaving the page.
The full page
The drawer's “expand” opens /notifications — a two-pane console (searchable list + detail) for managing everything, with deep-links (/notifications#id).
Emit a notification
One authenticated call (a service key or a user). Address it to one recipient or fan out to many. The initiating app (app_key) is attributed automatically for service callers.
POST /v1/notifications
{
"user_id": 42, // recipient (or "recipients": [1,2,3])
"app_key": "llm_manager", // which app initiated (auto for service keys)
"type": "llm_manager.budget.exceeded",
"title": { "en": "Approve a budget increase", "ar": "..." },
"body": { "en": "GPT-4o crossed 90% of its monthly cap." },
"category": "approval", // notification.category lookup
"priority": "high", // low | normal | high | urgent | critical
"interaction": "approve_reject", // the response UI to render
"action_required": true,
"config": { "require_rejection_reason": true },
"due_at": "2026-06-10T17:00:00Z",
"link": "/llm-manager"
}
Interaction types
Set interaction (+ a matching config) and the SDK renders the controls for you:
acknowledge · approve_reject · yes_no · single_choice · multiple_choice · custom_choice
text · file_upload · comment · composite · none (informational)
Choices live in config.choices [{id,title,description,recommended,risk}]; text takes config.max_length; composite takes config.components [{type,required}]. Governed fields (category / priority / interaction / status) are platform lookups — GET /v1/notifications/meta returns the live option lists.
Read, respond & manage
The recipient's surface uses GET /v1/notifications (inbox, filters incl. ?app_key=), /{id} (detail), POST /{id}/respond, /{id}/read, /read-all, /{id}/comments, /{id}/assign, /{id}/escalate, /{id}/cancel, and GET|PUT /v1/notifications/preferences. Every notification, response, comment and preference is also fully manageable from the Admin console (full CRUD + soft-delete + audit).
From scaffold to running
Apps use the mandated runtime — TypeScript on Expo for the frontend, FastAPI for the backend — and build against @stryde/superapp-sdk. You declare everything in the manifest; the platform wires it.
Scaffold apps/<app_key>/
Create manifest.json, frontend/ (the mount entry), and service/ (the backend). app_key namespaces everything.
Adopt the design system — link both SDK sheets
Link /sdk/stryde.css then /sdk/stryde-tokens.css (last) in your <head>, map your Tailwind colours to the SDK token names as HSL channels (bg-bg, text-ink, …), and build your surface with the .stryde-* classes + a .stryde-side side menu (no top nav — the shell owns it). That alone makes the app match the homepage. Download the kit and copy the starter scaffold from Downloads.
Write the backend
Expose /healthz + /readyz, a versioned API under /api/apps/<app_key>, trust only the internal token, and return code-based errors.
Declare i18n, lookups & error codes
Every user-facing key + error code, with full coverage for all active locales. Reference data goes in namespaced lookups, never hardcoded enums.
Wire observability
Initialize OTel; the SDK attaches the mandatory attributes automatically. Log through the SDK, not console.log.
Adopt the design system — the whole visual contract is two links + the .stryde-* classes:
<!-- app/layout.tsx <head> — tokens sheet LAST so the SDK wins -->
<link rel="stylesheet" href="/sdk/stryde.css?v=3"/>
<link rel="stylesheet" href="/sdk/stryde-tokens.css?v=3"/>
// Build with the canonical classes — they re-theme automatically:
<nav class="stryde-side"> … <a class="stryde-side-item active">Overview</a> </nav>
<span class="stryde-eyebrow">★ Section</span>
<h1 class="stryde-hero">Build it once. <span class="g">Look like Stryde.</span></h1>
<div class="stryde-card">…</div>
<button class="stryde-btn stryde-btn-primary">Primary →</button>
// manifest.json — declare it, or the gate (GATE.UI.DESIGN_SYSTEM) rejects you:
"frontend": { "design_system": "stryde@2",
"stylesheets": ["/sdk/stryde.css", "/sdk/stryde-tokens.css"] }
Native/federated apps additionally receive the injected sdk (sdk.api, sdk.i18n.t(), OTel attributes) and use the @stryde/design-system primitives — see app-standards.md §6. For the look, the two sheets above are all you need.
The single source of wiring
The platform validates it on registration and stores it versioned — immutable once deployed. Full schema is in app-standards.md §3.
{
"app_key": "invoicing",
"name": { "en": "Invoicing", "ar": "..." }, // locale-map, not a string
"version": "1.0.0", // semver, immutable once deployed
"frontend": { "route": "/invoicing", "mount": "default",
"nav": { "label_key": "invoicing.nav.title" },
"budget": { "max_bundle_kb": 250 } }, // enforced
"backends": [{ "api_base": "/api/apps/invoicing",
"health": { "liveness": "/healthz", "readiness": "/readyz" } }],
"permissions": [{ "key": "app.invoicing.view" }],
"observability": { "otel": true,
"mandatory_attributes": ["user_id","session_id","app_id","org_id","team_id"] }
}
Nothing deploys until it passes
Every manifest passes through an automated gate before it can deploy. A report with zero error-severity issues means pass. These checks are mechanical — the gate is the standard.
# dry-run any manifest → { "passed": true|false, "issues": [...] }
curl -X POST $API/v1/apps/validate -H 'Content-Type: application/json' --data @manifest.json
The pipeline & lifecycle
Deployment is admin-console-driven and gated. The backend deploys to the orchestrator; the frontend ships as a bundle the shell mounts. Rollback is repointing the manifest at the previous version.
# the deploy loop, end to end
curl -X POST $API/v1/apps/register --data @manifest.json # runs the gate → "validated"
curl -X POST "$API/v1/apps/myapp/deploy?version=1.0.0" # → "deployed"
curl -X POST $API/v1/apps/myapp/enable # → shows in the shell
curl -X POST "$API/v1/apps/myapp/rollback?version=0.9.0" # instant rollback
The gate rejects these
Most of these are caught automatically — but build with them in mind so you never hit the wall.
Will be rejected by the gate
- Hardcoded user-facing strings, or keys missing any active locale
- Un-namespaced or colliding permissions / events / lookups / error codes
- Direct fetch/axios, or no @stryde/superapp-sdk import (strict SDK)
- Missing /healthz or /readyz, or a wrong api_base
- OTel off or missing mandatory attributes; bundle over budget
Don't do it (policy / safety)
- Secrets in the frontend bundle or a git-committed manifest
- Translated error strings from the backend — emit codes only
- Hardcoded colors/spacing or physical left/right (breaks theming & RTL)
- Bypassing the gateway or trusting client-supplied identity/roles
- Mutating a deployed manifest version (it's immutable — ship a new one)
A good app, in one line
- Declares everything in its manifest, uses the SDK for every platform concern, localizes all text, emits codes + telemetry, stays in budget, and ships a new version to change anything.
Everything you need
Get the SDK kit, then follow the README. Every link below is live — they download the real files the platform serves.
The SDK is design_system stryde@2. Link both sheets (tokens last), build with .stryde-* and a .stryde-side side menu, declare design_system + stylesheets in your manifest — the gate (GATE.UI.DESIGN_SYSTEM) rejects anything that doesn't.
On the roadmap
Straight from the backlog. These are designed and specced — not yet wired.
soon Live shell on web + native
Wire the monorepo, boot Expo, render mounted apps in the browser and on device with OTA updates.
soon Auth & gateway
Clerk session → internal-token mint, service-to-service credentials, Traefik forward-auth, effective permissions.
soon Real orchestration
Deploy driver applies k3s manifests, health-gated rollout/rollback, image+bundle publish step.
soon Redis cache + pub/sub
Cross-instance cache coherence for lookups/permissions/theme — the must-fix for horizontal scale.
soon Configurable theming
Edit platform tokens live from the admin console; apps re-theme with no rebuild.
soon User-centric observability
Per-user activity timeline assembled from SigNoz — sessions, apps, traces, errors, latency.
soon Full admin console
App management, health, metrics, RBAC, lookups, i18n, error codes, theme, audit, notifications.
soon Phase-2 federation
Independent per-app frontend deploys via Module Federation (web) + Re.Pack (native).