Publish to the Sentroy App Store
Bring your own web app into Sentroy OS. Your app keeps running on your servers; Sentroy embeds it as a sandboxed iframe and hands it a short-lived signed identity token — no passwords, no shared cookies.
Overview#
A Sentroy App Store app is described by a single manifest file (<id>.sentroy-app.json). When a user installs it, the app appears in their Sentroy OS and opens in a sandboxed iframe pointed at your embed.url. Your app authenticates the user with a token Sentroy injects into the iframe URL, then talks to its own backend as usual.
The manifest#
Author <id>.sentroy-app.json. The file name must match identity.id. The schema is the single source of truth (validated in CI); the key rules: all URLs are https (no IPs), identity.version is strict semver and must increase on every update, and for auth.mode: "token" the jwksAudience origin must equal your embed.url origin.
{
"manifestVersion": 1,
"identity": {
"id": "resend",
"name": "Resend",
"version": "1.0.0",
"tagline": "Email for developers"
},
"appearance": {
"logoUrl": "https://app.resend.com/logo.png",
"color": "#0f0f0f",
"category": "developer-tools",
"screenshots": [{ "url": "https://…", "alt": "Dashboard", "width": 1280, "height": 800 }]
},
"embed": {
"url": "https://app.resend.com/sentroy",
"injectedParams": ["lang", "fallbackLang", "theme", "companySlug", "token"],
"sandbox": { "allowForms": true, "allowPopups": false },
"minHeight": 480
},
"auth": {
"mode": "token",
"jwksAudience": "https://app.resend.com"
},
"i18n": { "supportedLangs": ["en", "tr"], "fallbackLang": "en" },
"store": {
"description": "Send email with a developer-first API.",
"longDescription": "Longer description, plain text.",
"privacyUrl": "https://app.resend.com/privacy"
},
"developer": { "companySlug": "resend" },
"pricing": { "model": "free" },
"capabilities": { "requestsUserIdentity": true }
}Auth modes#
none — no identity is passed. token — Sentroy injects a short-lived RS256 JWT (recommended for most apps). oauth — full OAuth 2.0 / OIDC authorization-code flow for account linking; an OAuth client is created for you on approval and you request only the scopes you need.
Submitting your app#
Two ways, both landing in the same review queue:
1. Pull request — open a PR to github.com/Sentroy-Co/sentroy-apps adding apps/<id>.sentroy-app.json. CI validates the manifest against the schema.
2. Dashboard — submit the manifest from your Sentroy company dashboard. Either way developer.companySlug must be a Sentroy company you own or administer — this binds every app to a verified identity and blocks impersonation.
Verifying your origin#
Before an app goes live, Sentroy verifies you control the embed origin. Serve a file at:
https://<your-embed-origin>/.well-known/sentroy-app-verification.txtcontaining the token shown in your dashboard. The reviewer fetches it server-side and compares the first line.
Reading & verifying the embed token#
When token is in your injectedParams, Sentroy appends a fresh token to the iframe URL on every open. Read it, then verify it against Sentroy's JWKS.
// Inside your embedded page (e.g. https://app.resend.com/sentroy)
const params = new URLSearchParams(location.search)
const token = params.get("token") // short-lived (<=60s) identity JWT
const lang = params.get("lang") // active OS language
const company = params.get("companySlug") // active Sentroy company
// Strip the token from the URL so it doesn't linger in history.
if (token) history.replaceState(null, "", location.pathname)Verify the signature (RS256), the iss, and that aud equals your own origin — this rejects tokens minted for any other app.
import { createRemoteJWKSet, jwtVerify } from "jose"
// Sentroy publishes its public keys here.
const JWKS = createRemoteJWKSet(new URL("https://auth.sentroy.com/.well-known/jwks.json"))
export async function verifySentroyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.sentroy.com",
audience: "https://app.resend.com", // MUST equal your own origin
})
if (payload.typ !== "embed+jwt") throw new Error("not an embed token")
// payload.sub = Sentroy user id, payload.companySlug = active company,
// payload.email / name / picture present only if you requested those scopes.
return payload
}Security expectations#
Your app runs in an iframe sandboxed with allow-scripts allow-same-origin allow-forms(never top-navigation or modals), and your origin is added to Sentroy's CSP allow-list on approval. Never rely on the token sitting in the URL — strip it after reading. All security-relevant values are computed server-side from your reviewed manifest, so editing the file at runtime cannot widen your privileges.
Review & versioning#
After submission you receive email at each step (received → approved / changes requested). Updates bump identity.version (semver, monotonic) and re-enter review; the store keeps a version history. manifestVersionis a separate concept — it's the schema contract and you should not change it.