Skip to Content
Applications

Applications

Applications are under active development and are not yet stable. Breaking changes may happen between releases without warning. Feedback and bug reports are welcome via GitHub Issues .

Build a app when you need new operations. Build an application when that app becomes the backend for a user-facing workflow with UI, policy, storage, schedules, or other provider bindings.

Gestalt can be used to build applications. At a minimum, an application is a single app package that may include both executable backend code and a browser frontend, using existing primitives for identity, authorization configuration, IndexedDB/Cache/S3, workflows, and agents.

Application Shape

This example application, workspace_review, stores review items in IndexedDB, writes generated reports to S3, runs a nightly cron through the workflow provider, and serves a browser UI at /workspace-review from the same app package.

Use a single app repository with frontend source co-located in the same tree:

workspace-review/ manifest.yaml package.json package-lock.json provider.ts src/ app.tsx api.ts

The app manifest owns operations, build phases, and optional local run commands. For the full manifest schema, see Provider Manifests.

kind: app source: github.com/acme/apps/workspace-review version: 0.1.0 displayName: Workspace Review description: Review workspace changes and publish nightly summaries. install: - [npm, ci] build: - command: [npm, run, build] inputs: - package.json - package-lock.json - src - provider.ts run: - command: [npm, run, dev] readyTimeout: 120s - command: [npm, run, dev:provider] spec: mcp: true connections: default: mode: none auth: type: none

Check in frontend source and dependency lockfiles. Declare install, build, and run command lists in the manifest. The build phase writes static assets to GESTALT_BUILD_STATIC (see Building the Frontend); Gestalt copies that output into the packaged static/ directory. Engineers never name static/ in source — it exists only in prepared and release archives.

Unified app packages are the recommended shape for new applications. List-form install, build, and run phases are still evolving; explicit commands and build-tool availability in the preparation environment are required today.

A prepared release archive layout looks like:

manifest.yaml artifacts/ workspace-review-linux-amd64 static/ index.html assets/ app-*.js app-*.css entrypoint: artifactPath: artifacts/workspace-review-linux-amd64

For static mount and authorization configuration, see Serving and Mounts and Authorization.

Host Services

The deployment config wires the application into host services. Platform identity protects the mounted frontend and HTTP operation routes, the authorization policy controls who may use the application, and the provider bindings expose only the host services the app is allowed to use.

server: baseUrl: https://gestalt.example.com encryptionKey: ${GESTALT_ENCRYPTION_KEY} providers: identity: oidc authorization: authz indexeddb: main authorization: models: default: resourceTypes: workspace-review: relations: admin: subjectTypes: [subject] providers: identity: oidc: source: package: github.com/valon-technologies/gestalt-providers/auth/oidc version: 0.0.1-alpha.1 config: issuerUrl: https://login.example.com clientId: ${OIDC_CLIENT_ID} clientSecret: secret: provider: default name: oidc-client-secret authorization: authz: source: package: github.com/valon-technologies/gestalt-providers/authorization/indexeddb version: 0.0.1-alpha.1 indexeddb: provider: main db: authz indexeddb: main: source: package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb version: 0.0.1-alpha.1 config: dsn: ${DATABASE_URL} s3: reports: source: package: github.com/valon-technologies/gestalt-providers/s3/s3 version: 0.0.1-alpha.1 config: ... workflow: local: source: package: github.com/valon-technologies/gestalt-providers/workflow/indexeddb version: 0.0.1-alpha.1 default: true indexeddb: provider: main db: workflow config: pollInterval: 1s apps: workspace_review: source: ./workspace-review/manifest.yaml auth: provider: server authorizationPolicy: workspace_review static: mount: /workspace-review indexeddb: provider: main db: workspace_review objectStores: - items - reports s3: - reports capabilities: workflow: operations: - events.deliver workflows: definitions: nightly_summary: provider: local runAs: subject: id: service_account:workspace-review-nightly steps: - id: generate_report app: name: workspace_review operation: reports.generateNightly on: nightly: schedule: cron: "0 3 * * *" timezone: America/New_York

For the full configuration reference, see Config File. For workflow definitions and activations, see Providers > Workflow. For the service_account:workspace-review-nightly subject and runAs, see Service Accounts.

Application Logic

In app code, open host services from the request context or language SDK. The app does not know database DSNs, S3 credentials, or workflow storage details. Those come from the deployment bindings.

package provider import ( "context" "encoding/json" "net/http" "time" gestalt "github.com/valon-technologies/gestalt/sdk/go" "github.com/valon-technologies/gestalt/sdk/go/client" ) type Provider struct{} type SaveItemInput struct { ID string `json:"id" required:"true"` Title string `json:"title" required:"true"` Status string `json:"status" required:"true"` } type SaveItemOutput struct { OK bool `json:"ok"` } func (p *Provider) saveItem( ctx context.Context, input SaveItemInput, _ gestalt.Request, ) (gestalt.Response[SaveItemOutput], error) { db, err := gestalt.IndexedDB(ctx) if err != nil { return gestalt.Response[SaveItemOutput]{}, err } defer db.Close() items := db.ObjectStore("items") if err := items.Put(ctx, gestalt.Record{ "id": input.ID, "title": input.Title, "status": input.Status, "updatedAt": time.Now().UTC().Format(time.RFC3339), }); err != nil { return gestalt.Response[SaveItemOutput]{}, err } return gestalt.OK(SaveItemOutput{OK: true}), nil } type NightlyReportInput struct{} type NightlyReportOutput struct { ReportURL string `json:"reportUrl"` } func (p *Provider) generateNightly( ctx context.Context, input NightlyReportInput, req gestalt.Request, ) (gestalt.Response[NightlyReportOutput], error) { db, err := gestalt.IndexedDB(ctx) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } defer db.Close() s3, err := client.ConnectS3(ctx, "reports") if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } body, err := json.Marshal(map[string]string{ "generatedAt": time.Now().UTC().Format(time.RFC3339), }) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } ref := &client.S3ObjectRef{Key: "nightly/" + req.IdempotencyKey + ".json"} stream, err := s3.WriteObject(ctx, &client.WriteObjectOpen{ Ref: ref, ContentType: "application/json", }) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } if err := stream.Send(body); err != nil { return gestalt.Response[NightlyReportOutput]{}, err } if _, err := stream.CloseAndRecv(); err != nil { return gestalt.Response[NightlyReportOutput]{}, err } access, err := s3.PresignObject( ctx, client.PresignMethodGet, 900, ref, &client.S3PresignObjectOptions{ContentType: "application/json"}, ) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } if err := db.ObjectStore("reports").Put(ctx, gestalt.Record{ "id": req.IdempotencyKey, "url": access.Url, "createdAt": time.Now().UTC().Format(time.RFC3339), }); err != nil { return gestalt.Response[NightlyReportOutput]{}, err } workflows, err := gestalt.WorkflowFromContext(ctx) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } _, err = workflows.DeliverEvent(ctx, &client.WorkflowEvent{ Type: "workspace_review.report.generated", Source: "workspace_review", Subject: "nightly", Datacontenttype: "application/json", Data: map[string]any{"reportUrl": access.Url}, }, nil) if err != nil { return gestalt.Response[NightlyReportOutput]{}, err } return gestalt.OK(NightlyReportOutput{ReportURL: access.Url}), nil } var Router = gestalt.MustRouter( gestalt.Register( gestalt.Operation[SaveItemInput, SaveItemOutput]{ ID: "items.save", Method: http.MethodPost, }, (*Provider).saveItem, ), gestalt.Register( gestalt.Operation[NightlyReportInput, NightlyReportOutput]{ ID: "reports.generateNightly", Method: http.MethodPost, }, (*Provider).generateNightly, ), )

Calling Another App From Your Application

Use the generated App client when executable app code needs to call another configured app as part of the same request. Open it with the request-scoped helper for your language (gestalt.AppFromContext(ctx) in Go, request.app() in Python and TypeScript, App::connect().await?.with_context(...) in Rust). Hosted apps may call any other configured app without declaring a deploy-time dependency graph. The client sends the current request context to the host, which preserves the original subject, credential context, and permission ceiling for the nested call unless you pass per-request execution options.

apps: triage: source: ./apps/triage/manifest.yaml github: source: ./apps/github/manifest.yaml
import ( "context" gestalt "github.com/valon-technologies/gestalt/sdk/go" "github.com/valon-technologies/gestalt/sdk/go/client" ) func loadLinkedIssue(ctx context.Context, request gestalt.Request, issueNumber int) (map[string]any, error) { app, err := gestalt.AppFromContext(ctx) if err != nil { return nil, err } // Invoke decodes the standard JSON operation envelope; payload // failures return *client.InvokeError. Use InvokeRaw for the raw // status, headers, and body. return client.JSONResultAs[map[string]any](app.Invoke( ctx, "github", "issues.get", map[string]any{ "owner": "acme", "repo": "roadmap", "number": issueNumber, }, &client.AppInvokeOptions{ Connection: "default", IdempotencyKey: request.IdempotencyKey, RunAs: &client.SubjectContext{ ID: "service_account:nightly-sync", }, CredentialMode: "none", }, )) }

Pass connection, instance, credentialMode, runAs, or an idempotency key when the target app should use a specific connected account, provider instance, delegated service-account identity, credential resolution mode, or retry boundary. Agent tool calls and workflow steps still authorize their own targets; they cannot override authority with request runAs. For raw GraphQL surfaces, use InvokeGraphQL, invoke_graphql, or invokeGraphQL in the same SDK client; they return the raw result, and the SDK’s decodeGraphQLResult helpers apply the same envelope decoding plus GraphQL errors handling.

Building the Frontend

Frontend assets live in the same app repo as the provider. Gestalt does not serve TypeScript or JSX source at runtime — only the prepared static/ bundle.

Relative asset references

Use relative URLs in HTML, CSS, and JS (./assets/app.js, not /assets/app.js). Gestalt injects a <base href="/{mount}/"> tag when serving mounted apps so relative references resolve under the mount path. Client routers should derive their base from document.baseURI rather than hard-coding /.

During build, Gestalt sets GESTALT_BUILD_STATIC to an absolute path under .gestalt/build-static/ in the manifest directory. Your build script must write index.html and assets there. Gestalt verifies index.html exists after the build phase and stages the directory into static/ in release archives.

Vite

Use the Gestalt Vite plugin for base-path and dev-server defaults:

import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import gestalt from "@valon-technologies/gestalt/vite"; export default defineConfig({ plugins: [react(), gestalt()], build: { outDir: process.env.GESTALT_BUILD_STATIC, emptyOutDir: true, }, });

gestalt() sets base to a relative path, configures the dev server for GESTALT_DEV_* when gestaltd proxies local run commands, and proxies /api/v1 to GESTALT_BASE_URL for two-origin local dev.

Manual equivalent when not using the plugin:

export default defineConfig({ base: "./", server: { host: "127.0.0.1", port: Number(process.env.GESTALT_DEV_PORT), strictPort: true, }, build: { outDir: process.env.GESTALT_BUILD_STATIC, emptyOutDir: true, }, });

webpack

Set output.publicPath to "" (empty string) so chunk URLs stay relative.

Unsupported frameworks

Frameworks that emit absolute asset URLs only — including default Next.js and Astro production builds — are not compatible with Gestalt static mounts. Use a static-export mode with relative URLs, or a bundler that supports relative publicPath / base.

Serving and Mounts

Deployment config mounts prepared static assets with apps.<name>.static:

apps: workspace_review: source: ./workspace-review/manifest.yaml static: mount: /workspace-review
FieldDescription
static.mountPublic URL prefix. Defaults to /{app name} when omitted.
static.publicWhen true, serve the static bundle without login. Defaults to false.
static.themeOptional deployer workaround: paths to a theme stylesheet and assets directory injected before the static handler.

Only one app may mount at /.

Gestalt serves static files from the prepared static/ directory, applies SPA fallback (unknown paths under the mount return index.html), and injects <base href> into HTML responses when the document does not already define one. Theme endpoints (/theme.css, /theme/*) are handled before the static file handler when static.theme is configured.

Authorization

Mounted app static assets support three access tiers:

TierConfigurationBehavior
Publicstatic.public: trueNo login required to load HTML, CSS, JS, or theme assets. API calls still require a session.
AuthenticatedDefault (static.public omitted or false)Browser login required before any bytes are served from the mount.
AuthorizedauthorizationPolicy set (or app name as resource type)After login, Gestalt checks relationships before serving bytes. Unauthorized requests receive a login redirect or 403 without leaking asset contents.

When static.public: true, authorization policy and relationships do not gate static serving. They still apply to /api/v1/{app}/{operation} routes.

Example public docs mount:

apps: docs: static: mount: /docs public: true

Grant access for authenticated and authorized tiers with static relationships and optional default roles:

authorization: models: default: resourceTypes: workspace_review: defaultRole: viewer relations: viewer: subjectTypes: [subject] admin: subjectTypes: [subject] relationships: - subject: type: subject id: user:alice relation: viewer resource: type: workspace_review id: workspace_review

authorization.models.<model>.resourceTypes.<app>.defaultRole names the role assigned when the subject has a relationship to the app resource but no more specific relation matches. Operation-level role allowlists in apps.<name>.allowedOperations still apply to API calls; they do not create separate static route policies.

Local development uses the same gate: gestaltd serve proxies run commands only after the app passes the configured authorization policy.

Lightweight Frontend

The frontend can stay small because Gestalt already handles sessions, route authorization, and static asset serving. A mounted UI calls the app operations on the same origin:

export async function saveItem(item) { const response = await fetch("/api/v1/workspace_review/items.save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(item), }); if (!response.ok) { throw new Error(`Save failed: ${response.status}`); } return response.json(); }

Use Serving and Mounts and Authorization for mount and access configuration. Check frontend source and lockfiles into the app repo; Gestalt prepares static output during lock, sync, and package — do not commit .gestalt/build-static/ or packaged static/ trees.

Configuring Webhooks

Applications can expose product-specific webhooks through the same app that backs the UI, scheduled work, and host-service bindings. Declare the hosted HTTP route in the application app manifest, and implement webhooks.github in the SDK language of choice like any other application operation. The following example configures the public webhook URL as /api/v1/workspace_review/webhooks/github.

kind: app source: github.com/acme/apps/workspace-review version: 0.1.0 displayName: Workspace Review description: Review workspace changes and publish nightly summaries. spec: mcp: true connections: default: mode: none auth: type: none securitySchemes: github_webhook: type: hmac secret: env: GITHUB_WEBHOOK_SECRET signatureHeader: X-Hub-Signature-256 signaturePrefix: sha256= payloadTemplate: "{raw_body}" http: github_webhook: path: /webhooks/github method: POST credentialMode: none security: github_webhook target: webhooks.github requestBody: required: true content: application/json: {}

Local Development

Run the unified app locally from the app directory with supporting provider bindings in a development config file:

gestaltd provider validate --path ./workspace-review --config ./dev.yaml gestaltd serve ./workspace-review --config ./dev.yaml

When the app manifest declares run: commands and config sets apps.<name>.static.mount, gestaltd classifies the app for local development:

  1. Full-stack dev — a run command opens the provider socket on GESTALT_PROVIDER_SOCKET and (optionally) another run command binds a frontend dev server to GESTALT_DEV_PORT.
  2. Frontend-only dev — only the frontend binds GESTALT_DEV_PORT; gestaltd serves API routes from pinned artifacts or skips backend registration after a short readiness grace period.

gestaltd appends these environment variables to each run command (manifest env entries are applied first):

VariablePurpose
GESTALT_DEV1 while gestaltd manages the dev process
GESTALT_DEV_PORTEphemeral 127.0.0.1 port the frontend dev server must bind
GESTALT_DEV_BASE_PATHMount path from apps.<name>.static.mount
GESTALT_BASE_URLPublic gestaltd origin; consumed by gestalt() for /api/v1 proxying
GESTALT_PROVIDER_SOCKETUnix socket for the app provider (full-stack apps only)

The dev supervisor probes frontend readiness on GESTALT_DEV_PORT, restarts crashed commands with exponential backoff, and allows one dev binder per app name.

Package script recipe

{ "scripts": { "dev": "vite", "dev:provider": "tsx ./provider.ts", "build": "vite build" } }
run: - command: [npm, run, dev] readyTimeout: 120s - command: [npm, run, dev:provider]

Map framework dev servers to the Gestalt contract:

ToolPortBase path
Vite (gestalt() plugin)process.env.GESTALT_DEV_PORThandled by plugin
Vite (manual)--port $GESTALT_DEV_PORT --host 127.0.0.1base: './' in config
Angular (ng serve)--port $GESTALT_DEV_PORT --host 127.0.0.1--serve-path $GESTALT_DEV_BASE_PATH
webpack-dev-serverport: Number(process.env.GESTALT_DEV_PORT)devMiddleware.publicPath: process.env.GESTALT_DEV_BASE_PATH + '/'
Parcel--port $GESTALT_DEV_PORT --host 127.0.0.1--public-url $GESTALT_DEV_BASE_PATH/

For layered config, remote attach, and PATH overrides, see App > Testing locally and CLI > Local Frontend Development.