Platform

Architecture & routing

All app UI in a workspace runs on a single origin: {workspace}.saaslivery.com. The subdomain identifies the workspace, and each app lives under a path prefix, /app/{slug}/. A permanent shell loads once at / and opens apps as virtual tabs, so switching between apps never reloads the page.

Subdomain map

Subdomain Purpose
saaslivery.com / www Marketing site, signup, login
{workspace}.saaslivery.com Every installed app's UI, inside the shell, under /app/{slug}/
api.saaslivery.com All API endpoints, JSON only
apps.saaslivery.com Marketplace and this developer documentation
admin.saaslivery.com Platform operations console (internal)
rtc.saaslivery.com WebRTC media for calls and rooms

The workspace shell

Visiting {workspace}.saaslivery.com/ loads the shell once. When a member opens an app, the shell fetches the app's page as an HTML fragment, injects it into a tab panel, and keeps it alive in memory. Tab switching is instant; back and forward are handled by the shell's own history management.

What this means for an app:

  • Full pages and fragments are the same route. The platform detects fragment requests and swaps your template's base so the same handler serves both a direct browser hit and a shell tab load.
  • Never navigate with plain links between apps. A raw <a href="/app/other-app/"> triggers a full page load and destroys every open tab's state. In-shell navigation uses the shell's tab-opening event; within your own app, use HTMX swaps.
  • Your app shares the DOM with other open apps. Element ids you consider unique exist once per open tab, which is why HTMX requests must always carry a source element so swaps resolve inside the right tab.

Routing: two registration targets

An app registers UI routes on the wildcard subdomain with its /app/{slug}/ prefix, and API routes on the api subdomain namespaced by slug:

from heaven import App

app = App(debug=False)
app.TEMPLATES("templates", relative_to=__file__, prefix="bookmarks")

# UI: {workspace}.saaslivery.com/app/bookmarks/*
ws = app.subdomain("*")
ws.GET("/app/bookmarks/", "plugins.bookmarks.pages.index")
ws.GET("/app/bookmarks/partials/list", "plugins.bookmarks.pages.bookmark_list")

# API: api.saaslivery.com/bookmarks/*
api = app.subdomain("api")
api.GET("/bookmarks", "plugins.bookmarks.apis.api_list")
api.POST("/bookmarks", "plugins.bookmarks.apis.api_create")

The wildcard subdomain engine serves every workspace. Your handler does not care which workspace it is; the platform resolves that before your code runs.

Do not use a named subdomain for UI

Registering app.subdomain("bookmarks") would carve out a global subdomain, not a per-workspace app. All UI goes on app.subdomain("*") under the /app/{slug}/ prefix.

API middleware

Every API route automatically gets:

Middleware What it does
require_json Rejects POST/PUT/PATCH bodies that are not application/json
require_version Requires the X-Api-Version header (details)
Authentication Session cookie or Bearer token, including workspace API keys
CORS Preflight and cross-origin headers for the workspace origins

Workspace resolution

The subdomain is the workspace. Platform middleware reads req.subdomain, validates it against the signed-in session's workspace, and sets ctx.workspace_id:

acme.saaslivery.com/app/tasks/   → the Acme workspace's Tasks tab
globex.saaslivery.com/app/tasks/ → the Globex workspace's Tasks tab

A member of Acme who tries a Globex URL is rejected at Layer 2 before any app code runs.

Static assets

Route Source Use for
/public/* Platform's public/ directory design.css, logo, shared JS
/assets/* Your app's assets/ directory Your app's CSS and images

The platform registers both during app discovery. Apps never call app.ASSETS() themselves.

Session sharing

The session cookie is set on .saaslivery.com, so one login works across the workspace subdomain, the API, and the marketplace. Apps read ctx.session and never configure their own sessions.

Calling the API from app UI

App frontends use the platform's MicroAPI wrapper, which targets api.saaslivery.com with credentials and the version header already set:

const bookmark = await MicroAPI.post('/bookmarks', { url, title });

See HTMX patterns for the full interaction contract between HTMX, Alpine, and the API.