App SDKs
Build a platform-native app: the Python egav-app-sdk runtime, the TypeScript @egav/app-sdk, and the shared platform clients you compose alongside them.
Building an app: the two runtimes and the shared clients
An app SDK is what you build a platform-native app with — the runtime that mounts tiered routers, resolves tenant context, applies the access gates, and emits events through the outbox. There are two: a Python backend runtime (egav-app-sdk) and a TypeScript SDK (@egav/app-sdk) for React frontends. A Python app also composes a set of shared platform clients (egav-authz-client, egav-cp-client, …) for auth, config, audit and notifications.
This page is the build side. To consume the platform from an external program instead, use a client SDK (Go, Rust, PHP, Node).
Python app runtime — egav-app-sdk
egav-app-sdk (import package egav_app_sdk, Python 3.11+) is the backend runtime for a platform-native app. It builds a FastAPI app with create_saas_app(...), mounts the four route tiers via make_public_router / make_portal_router / make_cp_router / make_internal_router, resolves tenant and user through get_app_context, and re-exports the outbox primitives (EventEnvelope, enqueue_event, enqueue_event_if_listener). It also installs a console script, egav-app-sdk, that validates and publishes your manifest.
Two ways to start
create_saas_app(this page and the quickstart) is the shortest path: one call builds the app, mounts the tiers and the lifecycle routes, and resolves tenants.- The
egav-templatescaffold is a complete service:egav_base.create_app, the Routes → Service → CRUD layers, Alembic, Docker, CI and the full private pin set. It does not callcreate_saas_app, so mount the lifecycle routes yourself withegav_app_sdk.lifecycle.endpoints.make_lifecycle_router(app_code, registry)(see Provisioning).
Pick one. Copying code between the two means wiring the same thing twice. For an app you intend to run in production, start from the scaffold.
Install
Services pin the SDK as a GitHub tarball in requirements.txt (the ${GITHUB_PAT} is injected at build time). To hack on the SDK itself, install it editable with its dev extras. It is not published to PyPI — install it from the source repository or a local checkout.
Use
Every public symbol below is exported from egav_app_sdk/__init__.py. A protected portal route declares its gates as FastAPI dependencies and reads tenant identity from the injected AppContext. Validate and publish the manifest with the bundled CLI:
egav-app-sdk manifest validate path/to/app.manifest.yaml
egav-app-sdk manifest publish path/to/app.manifest.yaml --app-code my-saas# How a real service pins the Python app runtime (GitHub tarball at a release tag).
# Every private package the SDK depends on needs its own pin; copy the set from
# egav-template. The token is supplied by the build (BuildKit secret) or ~/.netrc,
# never committed. See the quickstart, step 1.
egav-app-sdk @ https://api.github.com/repos/djokodonev/egav-app-sdk/tarball/v0.114.1# Working on the SDK itself (from a checkout of egav-app-sdk):
pip install -e ".[dev]"from fastapi import Depends, Request
from egav_app_sdk import (
AppContext, get_app_context, requires_feature, enforce_quota,
make_portal_router, create_saas_app, load_manifest,
)
MANIFEST = load_manifest("app.manifest.yaml")
portal = make_portal_router(MANIFEST.identity.app_code)
@portal.post(
"/greetings",
dependencies=[
Depends(requires_feature("hello_world.write")), # plan feature -> 403
Depends(enforce_quota("hello_world.greetings.count")), # quota -> 429
],
)
async def create_greeting(request: Request, ctx: AppContext = Depends(get_app_context)):
body = await request.json()
# ctx.user_guid / ctx.tenant_application_instance_guid are resolved by the SDK
# (every AppContext field, plus the profile endpoints: /users-orgs)
...
app = create_saas_app(
manifest=MANIFEST,
settings=_Settings(),
instance_resolver=_instance_resolver,
routers={"public": public, "portal": portal, "cp": cp, "internal": internal},
)Shared platform clients
Beyond the app runtime, a Python platform app composes a set of local, path-installed packages — the shared clients every backend service imports. They are not published to PyPI: reinstall-egav-packages.ps1 pip-installs them into the running containers and restarts the services, so editing one package propagates everywhere without a Docker image rebuild. A service depends on them alongside egav-app-sdk (and the egav-base framework), then wires the five access gates and the platform integrations through their exported symbols. See Auth & the five gates for how the gate packages compose on a route, and Data placement for the egav_base.homes session dependencies your routes use to reach your catalog and organization data.
These seven ship from the monorepo as path installs, never a public registry — the import names below are what your service code writes.
| Package | Import | Role |
|---|---|---|
egav-authz-client | from egav_authz_client import RequireAuthz, RequireCPAuthz, AuthzClient | Permission checks against AuthZ. RequireAuthz(resource, action) authenticates the portal bearer and checks the permission in one FastAPI dependency; RequireCPAuthz is the CP-operator equivalent; AuthzClient / authorize are the direct call surface. |
egav-cp-client | from egav_cp_client import CPClient and from egav_cp_client.feature_gates import require_active_subscription, require_feature, require_quota | Typed Control Plane config client plus the subscription / feature / quota gates. CPClient resolves org config, effective features, service endpoints, and manifest-driven config; the feature_gates are FastAPI dependencies (enforce_quota is the imperative form). |
egav-audit-client | from egav_audit_client import AuditClient, post_audit_event, AuditAction, AuditCategory, AuditOutcome | Emit audit events to the Audit service over Kafka or REST. AuditClient.log(...) is the async surface; post_audit_event(...) is the sync REST helper; the enums keep classification consistent across services. |
egav-auth-client | from egav_auth_client import init_auth_dependencies, TokenClaims, PortalIdentity | Token validation. init_auth_dependencies() returns an AuthDependencies whose methods validate_bearer_cp, validate_portal_bearer, validate_x_api_key (and validate_bearer_jwt) are the route auth gates; TokenClaims / PortalIdentity are the resolved identities. |
egav-notifications-client | from egav_notifications_client import NotificationClient, NotificationMessage, EmailBuilder, SlackBuilder, InAppBuilder | Dispatch multi-channel notifications. NotificationClient.send(...) takes a NotificationMessage and returns a DeliveryResult; the channel builders are reached through the client (client.email / client.slack / client.in_app). |
egav-comments-client | from egav_comments_client import CommentsClient, Comment, CommentList | Back a comments feature in a host service — threaded comments, reactions, mentions and GDPR anonymize, in CP or tenant context, over sync and a-prefixed async methods. |
egav-list-params | from egav_list_params import parse_list_params, ListResponse, SearchBase | Shared pagination / search / sort query model — not an HTTP client. SearchBase powers BaseCRUD.list(); ListResponse / ListMeta are the standard list envelope. |
RequireAuthzplus thefeature_gatesdeps are the Python composition of the same five gates the client SDKs port to Go, Rust, PHP and Node — same order, same fail-closed contract. The full stack applied to a portal route is on SDKs & clients.
Compose them on a route
The auth validators and the permission gate come from two packages; a service wires them once in its dependency module.
from egav_auth_client import init_auth_dependencies
from egav_authz_client import RequireAuthz, RequireCPAuthz
_auth = init_auth_dependencies()
validate_portal_bearer = _auth.validate_portal_bearer # portal user -> 401
validate_bearer_cp = _auth.validate_bearer_cp # CP operator -> 401
validate_x_api_key = _auth.validate_x_api_key # service-to-service -> 401
# One dependency = authenticate the portal bearer + check the permission.
require_create_project = RequireAuthz("project", "create")Side integrations follow the same import-then-call shape.
from egav_audit_client import AuditClient, AuditAction
from egav_notifications_client import NotificationClient, NotificationMessage
# Audit (async): await AuditClient().log(action=AuditAction.CREATE, entity_type="project", ...)
# Notify (async): await NotificationClient().send(NotificationMessage(...)) -> DeliveryResultTypeScript app SDK — @egav/app-sdk
The TypeScript app SDK covers the frontend side of a platform-native app: manifest parsing and validation, tenant context, feature gates, field widgets, and an authenticated ApiClient that decodes the standard response envelope. (The field widgets are the same catalogue the Settings & config page describes for rendering manifest-declared config fields.)
It is published to GitHub Packages under its real name, @djokodonev/egav-app-sdk (currently 0.27.0). Consumers reference it through the friendlier alias @egav/app-sdk using an npm alias in package.json — so imports read @egav/app-sdk while the resolved package is @djokodonev/egav-app-sdk.
Install
GitHub Packages is a private registry, so you need an .npmrc that routes the @djokodonev scope to it with an auth token, then the aliased dependency line.
Import
Import from the @egav/app-sdk alias. Real exported symbols include ApiClient / createApiClient, and loadManifest / validateManifest (subpath exports such as @egav/app-sdk/field-widgets and @egav/app-sdk/config-v2 are declared in the package's exports map).
@djokodonev:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}{
"dependencies": {
"@egav/app-sdk": "npm:@djokodonev/egav-app-sdk@^0.27.0"
}
}import { ApiClient as SdkApiClient } from '@egav/app-sdk';
// The alias resolves to @djokodonev/egav-app-sdk. Manifest helpers come from
// the same package: import { loadManifest, validateManifest } from '@egav/app-sdk';Typed service clients
ApiClient is the generic authenticated fetch layer; on top of it the SDK ships typed, per-service clients so a frontend never hand-rolls an endpoint. Build one from a configured ApiClient:
| Client | Use it for |
|---|---|
createCommentsClient | Threaded comments on any object (contextType + contextId) |
createNotifyClient | The in-app notifications inbox |
createFilesClient | Pre-signed uploads and downloads |
createDataExchangeClient | Import / export jobs |
createCpClient | Control Plane config + instance info |
createAuthzClient | Permission checks |
Comments can be attached to anything, by any app. The comments client scopes to a (contextType, contextId) pair — a project, a ticket, a row, any object your app owns — so any frontend can drop a threaded, mentionable feed onto it. Comments runs as its own multi-tenant service, so you store nothing: point the client at an object and it owns the thread. Its Python counterpart, egav-comments-client, does the same server-side.
import { createApiClient, createCommentsClient } from '@egav/app-sdk';
const api = createApiClient({ /* baseUrl + token source; injects X-App-Instance-Guid */ });
const comments = createCommentsClient(api);
// Attach a thread to any object, addressed by (contextType, contextId):
const { data, meta } = await comments.list('project', projectGuid, { page: 1, limit: 20 });
const posted = await comments.create('project', projectGuid, { body: 'Ship it 🚀' });
await comments.delete('project', projectGuid, posted.guid);The list returns the standard { data, meta } envelope; each Comment carries author_guid / author_name, body and timestamps. The platform's own frontends render this with a CommentsPanel component — copy that pattern, or build your own UI on the client.
Export any listing in the background. The data exchange client turns any list your app exposes — records, tickets, audit rows — into an async CSV / XLSX / JSON export. startExport returns a job guid; poll status until its state settles (succeeded / failed), then download the file over a pre-signed URL. cancel stops a running job. It is backed by the egav-data-exchange service — "background export/import for any listing in the platform."
import { createDataExchangeClient } from '@egav/app-sdk';
const dx = createDataExchangeClient(api); // api = a configured ApiClient (as above)
const { guid } = await dx.startExport({ listing: 'entities', format: 'xlsx', filters: { status: 'active' } });
let job = await dx.status(guid); // poll until the async job settles
while (job.state === 'pending' || job.state === 'running') job = await dx.status(guid);
const file = await dx.download(guid); // a Blob, fetched via a pre-signed URLNote the naming:@egav/app-sdk(this page) builds a React app;@egav/sdk(the Node client) only consumes the platform. Different packages, different jobs.