Most cloud platforms lock you into a single-tenant model. Your app gets one set of credentials. Every user's data, every provisioned resource, every database lives inside your workspace. You pay for it, you own it, and you carry the blast radius when something goes wrong.
Open Source Cloud (OSC) works differently. OSC is a managed platform for open source services: databases, media pipelines, communication tools, storage, and more. Every OSC user has their own workspace with their own token budget. If you build an app on OSC, you can let each visitor operate in their own workspace. They spin up services, they consume their own tokens, and your code never touches their credentials.
The mechanism is standard OAuth 2.0 with PKCE. The resulting token is a Personal Access Token (PAT) scoped to that user's workspace. This post shows you exactly how it works.
When to use this pattern
OSC gives you three ways to authenticate:
| Approach | Use when |
|---|---|
| Personal PAT from dashboard | Your own scripts and tooling |
| Platform API key | Server-to-server, operating in your own workspace |
| OAuth act-on-behalf | You publish an app for other OSC users to sign in to |
If you are building something others will use, and those users should control their own resources, the OAuth flow is the right choice.
Two live examples use this exact pattern:
- Open Intercom Site (live): visitors deploy and manage cloud intercom systems in their own OSC workspace.
- Open Media Convert (live): visitors transcode video using storage and encoding services provisioned in their own workspace.
Both are Express.js apps deployed on OSC My Apps. Both use the same four-step flow.
The flow in plain terms
- Your app generates a PKCE pair and a random state value.
- Your app redirects the user to
https://app.osaas.io/api/connect/authorize. - OSC shows a consent page. If the user allows, OSC redirects back to your callback with a one-time code.
- Your server exchanges the code for an access token and a refresh token. The access token is a PAT for that user.
PKCE is not optional. code_challenge_method must be S256. There is no option to skip it.
Registering your app
Go to the OSC dashboard, open My Apps, and click the OAuth Apps tab. Create an app. OSC generates a client_id (format: osc_<24chars>) and a client_secret (format: osc_secret_<64hex>). Copy the secret immediately: it is shown once.
If you want to skip manual registration, the platform also supports dynamic client registration via POST /api/connect/register. The live example apps use this as a fallback when CLIENT_ID and CLIENT_SECRET environment variables are not set.
Step 1: Generate PKCE and state
import crypto from "node:crypto";
function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
return { codeVerifier, codeChallenge };
}
function generateState(): string {
return crypto.randomBytes(16).toString("base64url");
}
Store codeVerifier and state in your server-side session before redirecting. Never send codeVerifier to the browser.
Step 2: Redirect to the consent page
Here is a sign-in handler for an Express.js app. It checks for pre-configured credentials, falls back to dynamic registration, then builds the redirect:
app.get("/auth/signin", async (req, res) => {
const redirectUri = getRedirectUri(req);
if (process.env.CLIENT_ID && process.env.CLIENT_SECRET) {
req.session.clientId = process.env.CLIENT_ID;
req.session.clientSecret = process.env.CLIENT_SECRET;
} else {
const client = await registerClient(redirectUri);
req.session.clientId = client.client_id;
req.session.clientSecret = client.client_secret;
}
const { codeVerifier, codeChallenge } = generatePKCE();
const state = generateState();
req.session.codeVerifier = codeVerifier;
req.session.oauthState = state;
const params = new URLSearchParams({
response_type: "code",
client_id: req.session.clientId!,
redirect_uri: redirectUri,
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
});
req.session.save(() =>
res.redirect(`https://app.osaas.io/api/connect/authorize?${params.toString()}`)
);
});
If the user is not signed in to OSC, the platform handles that step automatically before showing the consent screen.
Step 3: Handle the callback and exchange the code
app.get("/auth/callback", async (req, res) => {
const { code, state, error } = req.query;
if (error) {
res.status(400).send("Authorization denied");
return;
}
if (state !== req.session.oauthState) {
res.status(400).send("Invalid state parameter");
return;
}
const body = new URLSearchParams({
grant_type: "authorization_code",
code: code as string,
redirect_uri: getRedirectUri(req),
client_id: req.session.clientId!,
code_verifier: req.session.codeVerifier!,
client_secret: req.session.clientSecret!, // omit for dynamic/UUID clients
});
const tokenRes = await fetch("https://app.osaas.io/api/connect/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
const tokens = await tokenRes.json();
// {
// access_token: "...",
// refresh_token: "...",
// token_type: "Bearer",
// expires_in: 3600
// }
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.tokenExpiresAt = Date.now() + (tokens.expires_in - 60) * 1000;
delete req.session.codeVerifier;
delete req.session.oauthState;
req.session.save(() => res.redirect("/"));
});
Store both tokens in your server-side session. The access token expires after 3600 seconds.
Step 4: Call OSC APIs as the user
The access token is a standard OSC PAT. Pass it to the @osaas/client-core SDK and every API call runs inside the user's own workspace:
import { Context, listInstances, createInstance } from "@osaas/client-core";
const ctx = new Context({ personalAccessToken: req.session.accessToken });
const sat = await ctx.getServiceAccessToken("eyevinn-intercom-manager");
const instances = await listInstances(ctx, "eyevinn-intercom-manager", sat);
Service instances created through this context appear in the authenticated user's workspace and consume tokens from their plan, not yours.
Handling token refresh
For long-running operations like media transcoding, you need to refresh the access token before it expires. An ensureValidToken middleware handles this transparently:
async function ensureValidToken(req: express.Request): Promise<boolean> {
if (!req.session.accessToken) return false;
const now = Date.now();
const expiresAt = req.session.tokenExpiresAt;
if (expiresAt && now >= expiresAt) {
try {
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: req.session.refreshToken!,
});
const tokenRes = await fetch("https://app.osaas.io/api/connect/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
const tokenResponse = await tokenRes.json();
req.session.accessToken = tokenResponse.access_token;
if (tokenResponse.refresh_token) {
req.session.refreshToken = tokenResponse.refresh_token;
}
if (tokenResponse.expires_in) {
req.session.tokenExpiresAt =
Date.now() + (tokenResponse.expires_in - 60) * 1000;
}
return true;
} catch {
delete req.session.accessToken;
delete req.session.refreshToken;
delete req.session.tokenExpiresAt;
return false;
}
}
return true;
}
The refresh grant posts to the same token endpoint with grant_type=refresh_token. No client_id or code_verifier needed. Both tokens are rotated on each refresh.
What you can build with the PAT
Once you have the PAT, the user's entire OSC workspace is available to your app:
- Create and delete service instances (databases, media servers, SFUs, storage buckets, and 200+ other services in the catalog)
- List what is already running in the user's workspace
- Read logs from instances
- Check the user's plan and token balance
- Deploy apps via My Apps
Everything that a user can do manually in the dashboard, your app can do programmatically on their behalf.
Security notes
Two things to get right:
State parameter. Always generate a cryptographically random value, store it in the server-side session, and verify it exactly on the callback before processing the code. This is your CSRF protection for the callback endpoint.
Token storage. Access and refresh tokens live in your server-side session only. Never write them into HTML, JavaScript bundles, cookie values accessible to client code, or AI chat prompts. The last one matters more than you might expect: if a PAT ends up in a prompt, it gets stored in conversation history and forwarded to whatever LLM your app uses.
Try it now
The two live apps are running right now:
- intercom.apps.osaas.io: sign in and spin up a cloud intercom in seconds
- mediaconvert.apps.osaas.io: transcode a video using OSC services in your own workspace
To build your own: register an OAuth app in the OSC dashboard and deploy on My Apps. Full reference documentation, including the complete token refresh and redirect URI patterns, is at docs.osaas.io.
If you build something with this pattern, share it. The OSC MCP server supports the same OAuth flow, so your app can also work with AI agents acting on behalf of users. That is a pattern worth a post of its own.
Top comments (0)