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.