lace-app-sdk
Routes & runtime UI
Expose HTTP routes and federated UI modules from your app sidecar. Routes mount under /apps/<app_id>/api/* and survive restarts; UI loads as a Module Federation remote with no shell rebuild.
Routes — AppRouteProvider
Routes are declared by a provider class with app_id, a route_metadata() list (pure data — core projects it without importing your code), and a register_routes(context) that receives the sidecar's FastAPI app and app_state.
from lace_app_sdk.routes import AppRouteProvider, AppRouteContext, AppRouteMetadata
from lace_app_sdk.data import AppDataService, AppDataQueryRequest
from fastapi.responses import JSONResponse
class FieldIntakeRoutes(AppRouteProvider):
app_id = "acme.field_intake"
def route_metadata(self):
return [AppRouteMetadata(app_id=self.app_id, route_id="list_tickets",
kind="public", path_prefix="/tickets")]
def register_routes(self, context: AppRouteContext):
app = context.app
state = context.app_state
@app.get("/tickets")
async def list_tickets(limit: int = 20):
svc = AppDataService(app_state=state)
res = svc.list_records(self.app_id, "tickets", AppDataQueryRequest(limit=limit))
return JSONResponse({"tickets": [r.data_json for r in res.records]})
- Mount point:
/apps/<app_id>/api/<your-path>. The platform'sLACE_APP_PATH_PREFIXis injected bySidecarActivator. - Use
AppDataService(app_state=state)— not an in-memory dict — soGET /ticketssurvives sidecar restarts (see data collections dual-plane rule). - Register the collection at boot (idempotent) before serving:
AppDataService(...).register_collection(TICKETS). - Litmus test:
curl /apps/acme.field_intake/api/ticketslocally vialace-app devand again afterdocker restart— same rows.
Runtime UI — federated modules
The LACE shell never rebuilds for your app. Instead it loads your UI as a Module Federation remote at /ui/manifest.json (DEPLOY-007). The shell fetches lace_app_manifest.json → runtime_ui, then hydrates the remote entry.
// ui/src/RuntimeView.tsx — federated module exposed at ./RuntimeView
export default function RuntimeView() {
return <div>Tickets: {tickets.length}</div>
}
// lace_app_manifest.json → runtime_ui block
{
"runtime_ui": {
"module_id": "field_intake.ui",
"module_version": "1.0.0",
"entrypoints": {"session_home": "./RuntimeView"},
"isolation_mode": "iframe"
}
}
| Field | What it means |
|---|---|
module_id | Stable id, e.g. field_intake.ui |
entrypoints | At least one of new_run / session_home / run_detail → exposed module path (e.g. "./RuntimeView") |
isolation_mode | host (default) or iframe |
sdk_api_min / sdk_api_max | Host SDK contract range (default 1..1) |
runtime_contract_version | lace.ui_runtime.v1 |
Local verification
lace-app dev— routes mounted; UI manifest served at/ui/manifest.json.curl http://localhost:8080/ui/manifest.json | jq— expectremote_entry_url(set viaLACE_APP_UI_BUNDLE_BASE_URL+LACE_APP_UI_DIGESTin prod).- In prod, upload the UI bundle to
s3://.../app-bundles/<app_id>/<digest>/ui; the sidecar serves the manifest and the shell loads the remote entry.
App-scoped secrets & settings
Read secrets declared via get_secret("my_key") / get_secret_optional / get_setting from lace_app_sdk.secrets. They resolve through the platform's secret backend (ECS sm_secrets in cloud, filesystem in dev). Write them with lace-app secrets set <app_id> <key> <value>.
Next: testing & proof lanes or publishing.