Declare platform integrations in one versioned app manifest
The YAML app manifest is a versioned contract for features, events, authorization resources, audit types, notification templates, AI tools and supported AWS infrastructure. Publishing registers those declarations; SDKs for Python, TypeScript, Go, Rust and PHP connect application code to the platform. SDK access comes with your platform deployment.
Use the platform from the language you already build in
Sign-in and token refresh, service routing, the platform's typed responses and the same five access gates come built in, so identity, authorization, billing, quotas and events are plain function calls in your language.
Connect signup, tenant provisioning and the customer portal
Your site sends registrations to the platform. The Control Plane creates the organization and tenant instance, and customers use a portal with your brand and domain.
Connect your site, capture sign-ups
Point your sign-up at the platform through the SDK or hosted routes. Password registration stays in your product UI; social and enterprise sign-in route to the configured identity provider. Each registration creates an account and organization on the platform.
Identity & sign-in →Sign-up creates the tenant record and starts provisioning
The Control Plane creates the identity, organization, plan, subscription, quota and feature state, then starts the selected infrastructure workflow. Your app lifecycle handler prepares its schema and baseline data.
Billing & subscriptions →Hand them a portal you have branded
Your customers get a white-label portal that is already built — profile, billing, invoices, payment methods, MFA, support and notifications. Set your logo, colours, name and domain in the Control Plane and they apply on the next load, with no rebuild. Fork the branded starter when you want to change copy, layout or fonts too.
Customer portal →A subscription starts the tenant provisioning workflow
When a customer subscribes, the Control Plane creates the tenant instance and dispatches the selected infrastructure allocation to Fleet. Your app's lifecycle handler prepares its schema and baseline data, then acknowledges readiness.
What the platform provisions
- The tenant record, subdomain and request routing
- Storage for the isolation tier you declared — a shared pool by default, a dedicated database when the plan calls for one
- Credentials and per-tenant configuration, stored where your app reads them
- The roles and permissions your app declares, seeded into the authorization service for that tenant
- The customer notification when their instance is ready
- Supported AWS resources declared by the app and selected by the plan, provisioned through Fleet
What your app does on its end
Register two handlers and the SDK exposes them as secured internal endpoints. At provision time the platform calls yours, you prepare the tenant — run migrations, seed the baseline — and acknowledge. The instance is only marked ready once you do; at cancellation the same handshake runs in reverse, and teardown waits until your cleanup succeeds.
Read the full provisioning contract →backend/main.py
Two lifecycle handlers prepare and remove the product-specific tenant data.
from egav_app_sdk import (
LifecycleContext, LifecycleRegistry, create_saas_app, load_manifest,
)
lifecycle = LifecycleRegistry()
@lifecycle.on("provision")
async def on_provision(ctx: LifecycleContext, payload: dict) -> dict:
# The platform already created this tenant's storage and stored its
# credentials. Your job is what only you can do: schema + baseline.
await run_migrations(ctx.tenant_application_instance_guid)
await seed_baseline(org_guid=ctx.org_guid)
return {"ready": True} # the instance is promoted on your ack
@lifecycle.on("deprovision")
async def on_deprovision(ctx: LifecycleContext, payload: dict) -> dict:
# Teardown remains pending until this cleanup succeeds.
await remove_tenant_data(ctx.tenant_application_instance_guid)
return {"cleaned": True}
class Settings: # what create_saas_app reads from your settings
service_code = "my-app"
version = "1.0.0"
environment = "prod"
cors_origins = ""
async def resolve_instance(*, instance_guid, request):
# Required: map the request's instance to (subscription snapshot, identity).
# Production apps back this with egav-cp-client; return None for unknown.
return await lookup_subscription_and_identity(instance_guid, request)
app = create_saas_app(
manifest=load_manifest("backend/app.manifest.yaml"),
settings=Settings(),
instance_resolver=resolve_instance,
routers={"portal": portal},
lifecycle_handlers=lifecycle, # mounts the secured internal endpoints
)app.manifest.yaml
This versioned YAML file defines the contracts the platform registers.
apiVersion: platform.synaptagrid.io/v1 # const
kind: Application # const
metadata:
name: my-saas # machine code, ^[a-z][a-z0-9-]*$
display_name: My SaaS
version: 1.0.0 # semver
publisher: getek
spec:
# Features are DEFINITIONS. The Control Plane binds per-plan values
# (Starter max_projects=5; Pro max_projects=unlimited, sso=true) —
# plans live in CP, not the manifest.
features:
- code: max_projects
name: Max projects
description: Number of projects an organization can create.
ui_hint: Cap on active projects per org.
type: integer
default: 5
quota:
source: cumulative # cumulative | daily | per_request
policy: block # reject the next create at the limit
- code: sso
name: SSO
description: Allow the organization to sign in with enterprise SSO.
ui_hint: Toggle ON to enable SSO (Pro plan).
type: boolean
default: false
# ReBAC resource types — actions become AuthZ permissions on publish.
resource_types:
- code: project
name: Project
description: A project owned by an organization.
actions: [read, create, update, delete]
# Kafka topic ownership — required once the app emits events.
kafka:
owned_topic_patterns:
- my-saas.*
events:
emits:
- id: my-saas.project.created
topic: my-saas.project.created
schema_ref: schemas/events/project-created.v1.json
version: 1
audience: tenant-private # producer's org only
- id: my-saas.project.archived
topic: my-saas.project.archived
schema_ref: schemas/events/project-archived.v1.json
version: 1
audience: tenant-private
consumes:
- id: control-plane.subscription.plan_changed
topic: control-plane.subscription.plan_changed
schema_ref: schemas/events/plan-changed.v1.json
producer: control-plane
handler_endpoint: /v1/my-saas/internal/events/plan-changed
audit:
event_types:
- code: project.created
description: A project was created by an org member.
- code: project.deleted
description: A project was soft-deleted by an org member.
notification_templates:
- code: project_invite
channel: email
name: Project invitation
scope: tenant_overridableVersioned schema rejects invalid contracts
Strict-by-default, versioned validation. Plans, features, events, topics, authorization resources, audit catalog, notifications, GDPR endpoints, infra ownership.
Publication registers platform catalogs
Apps register on deploy. Publication synchronizes authorization resources, audit types, notification templates and plan features.
Declared events configure delivery
The event declarations configure internal event delivery and signed external webhooks, including their versioned payload schemas.
Plan-aware features and quota reporting
Feature flags follow the active plan and per-tenant overrides, with no bespoke gating in your code. Apps report metered usage to the Control Plane, and billing rolls it up into invoices.
GDPR endpoints
Declare the export and erasure endpoints. The Control Plane fans out user deletion across the platform services and every installed app that declares them.
SDKs for app runtimes and API clients
App runtimes for building platform-native apps (Python, TypeScript), plus API client SDKs to call the platform from TypeScript, Go, Rust and PHP.
The five access gates in each supported SDK
Whatever you build in, you protect an endpoint the same way: authenticate the user, check the permission, confirm the subscription is active, confirm the plan has the feature, and stay under the quota. Python and TypeScript get the full app runtime with base classes, cross-service clients and sign-in. Go, Rust and PHP get the client SDK with sign-in and token refresh, service routing, typed responses and the same five gates, with one typed error per failure.
The app runtime: platform wiring, your domain logic
from fastapi import Depends
from egav_base import AsyncBaseService, AsyncBaseCRUD
from egav_base.async_database import async_transaction_context
from egav_app_sdk import (
get_app_context, requires_permission, requires_feature, enforce_quota,
)
from .models import Project
from .schemas import ProjectCreate, ProjectUpdate, ProjectResponse
class ProjectCRUD(AsyncBaseCRUD[Project]):
model = Project
class ProjectService(
AsyncBaseService[Project, ProjectCreate, ProjectUpdate, ProjectResponse, ProjectCRUD]
):
model = Project
crud = ProjectCRUD
response_schema = ProjectResponse
# list/search/sort/pagination, soft-delete, audit — all inherited
# Three gates compose declaratively on one route:
@router.post(
"/v1/my-saas/portal/projects",
dependencies=[
Depends(requires_permission("project.create")), # AuthZ permission gate
Depends(requires_feature("projects")), # plan has the feature
Depends(enforce_quota("max_projects")), # under the quota
],
)
async def create_project(
payload: ProjectCreate,
ctx = Depends(get_app_context), # resolved tenant + user
db = Depends(get_db),
):
async with async_transaction_context(db): # commit happens here
return await ProjectService(db).create(payload)The five gates, enforced against the platform
from fastapi import APIRouter, Depends, Request
from egav_authz_client import RequireAuthz
from egav_cp_client.feature_gates import (
require_active_subscription,
require_feature,
require_quota,
)
router = APIRouter()
async def project_count(request: Request) -> int:
# Usage probe for the quota gate — what the org already consumed.
return await count_projects(request.state.identity.org_guid)
# The reference implementation: five gates declared once on the route.
# RequireAuthz authenticates the user's bearer and checks the permission
# against AuthZ; one cached entitlements fetch from the Control Plane
# powers the subscription, feature and quota gates.
@router.post(
"/v1/my-saas/portal/projects",
dependencies=[
Depends(RequireAuthz("project", "create")), # 1 · auth + 2 · permission
Depends(require_active_subscription), # 3 · subscription -> 402 when lapsed
Depends(require_feature("projects")), # 4 · feature -> 403 when not in plan
Depends(require_quota("max_projects", current_usage=project_count)), # 5 · quota -> 429
],
)
async def create_project(payload: ProjectCreate):
# Gates passed — do the work. Each failed gate has already returned
# 401 / 402 / 403 / 429 (or 503 when the platform is unreachable),
# in the platform's standard error envelope.
return await save_project(payload)Map your app to the platform contracts
Bring an OpenAPI specification or domain model. We will map its features, permissions, events and infrastructure requirements into the versioned app contract.