Platform

What is an app?

A Saaslivery app is a small, self-contained productivity tool that does one thing well. It has its own tab in the workspace shell, its own database tables, its own templates, and its own manifest. It plugs into the platform as a Heaven plugin and gets auth, sessions, tenancy, permissions, and asset serving for free.

The micro philosophy

Traditional productivity suites are monoliths: one giant app that does everything. Saaslivery takes the opposite approach.

  • Tasks manages boards and assignments. It doesn't know about leave requests.
  • Leaves manages time off. It doesn't know about calendars.
  • Calendar manages events. It doesn't know about mail.

Each app is simple on its own. Complexity comes from connecting apps through the event bus, not from bloating individual apps. When leave is approved, the Calendar app hears about it and blocks the dates. When a task is completed, any listening app can react.

The 10-table rule

Every app owns a maximum of 10 database tables. This is a hard constraint, checked at review time.

If your app needs more than 10 tables, it is doing too much. Decompose it into smaller apps that communicate via events.

Guideline Detail
Maximum owned tables 10 per app
Junction tables Count toward the limit
Projection tables Local read models of another app's events do not count, but follow the projection rules
workspace_id column Required on every table
Enforcement Validated at app review time

App structure

Every app follows the same directory layout; the full tour is in Project structure:

my-app/
├── __init__.py          # Empty, makes it a Python package
├── manifest.py          # Identity, dependencies, events, permissions, pricing
├── plugin.py            # Heaven App() + route registration
├── schema.py            # pytastic TypedDict schemas
├── helpers.py           # Constants, row converters, shared logic
├── apis.py              # API handlers (JSON responses)
├── pages.py             # UI pages + HTMX partials (HTML responses)
├── authz.py             # AUTHZ dict + authorization functions
├── events.py            # Event handlers (optional)
├── forms.py             # Form definitions (optional)
├── migrations/          # Database schema (max 10 tables)
├── templates/           # Flat; namespaced by the TEMPLATES prefix
└── assets/              # App-specific CSS, images (served at /assets/*)

Key rules

  • Self-contained. No app imports from another app's directory. Cross-app communication goes through the event bus only.
  • Split handlers. HTML in pages.py, JSON in apis.py, shared logic in helpers.py, authorization in authz.py. Never a single handlers.py.
  • Flat, prefixed templates. app.TEMPLATES("templates", relative_to=__file__, prefix="my-app") namespaces template names without nesting directories.
  • No debug mode. Child apps must use App(debug=False); the platform owns debug tooling.
  • No ASSETS, CORS, or sessions. The platform registers these globally.

Apps are Heaven plugins

The platform auto-discovers apps at startup. It scans apps/ (core) and plugins/ (third-party) for directories containing a plugin.py, imports them, and mounts them.

from heaven import App

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

# UI: {workspace}.saaslivery.com/app/tasks/*
ws = app.subdomain("*")
ws.GET("/app/tasks/", "apps.tasks.pages.index")
ws.GET("/app/tasks/board/:board_id", "apps.tasks.pages.board")

# HTMX partials: same wildcard subdomain
ws.GET("/app/tasks/partials/task-list", "apps.tasks.pages.task_list_partial")

# API: api.saaslivery.com/tasks/*
api = app.subdomain("api")
api.GET("/tasks", "apps.tasks.apis.api_list_tasks")
api.POST("/tasks", "apps.tasks.apis.api_create_task")

When mounted, the app gets:

Feature Provided by
A tab in the workspace shell at /app/{slug}/ Platform auto-discovery
Shared sessions across all subdomains Platform session middleware
CORS on the API subdomain Platform CORS handler
/public/* platform assets Platform asset registration
/assets/* app-specific assets Platform asset registration
ctx.workspace_id on every request Platform tenant middleware
Permission loading + the authz gate Platform security middleware

Core apps vs. third-party apps

Aspect Core apps Third-party apps
Location apps/ directory plugins/ directory (git submodule)
Publisher The platform Developer's registered publisher ID
Event namespace core.{app}.* ext.{publisher}.{app}.*
Permission keys {slug}.* ext.{publisher}.{slug}.*
Review Internal Platform team reviews code before deploy
Pricing Included in workspace tiers Set by the developer (80/20 split)

Both follow the exact same structure, rules, and isolation guarantees.

What an app cannot do

Action Why not
Import from another app Use the event bus instead
Set ctx.workspace_id Platform-controlled, read-only
Register its own sessions Platform handles sessions globally
Register CORS Platform handles CORS on the API subdomain
Use App(debug=True) Causes livereload collision on mount
Own more than 10 tables Decompose into smaller apps
Use a JavaScript build step HTMX + Alpine.js only

Next steps