Skip to main content
App SDK

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.

SDKs

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.

PythonTypeScriptGoRustPHP
How it fits together

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.

01

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 →
02

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 →
03

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 →
Provisioning

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_overridable

Versioned 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.

In your code

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.