Skip to Content

Apps

App providers are the packages that expose tools through Gestalt. Operators configure them under apps, while the host takes care of catalog loading, external credential resolution, connection management, HTTP routing, and MCP exposure.

How app providers work

An app package includes a manifest that defines metadata, connections, and optional passthrough API surfaces. Apps can be executable, declarative, or both. Apps support:

REST/OpenAPI, GraphQL, MCP and executable custom-defined code

as stated in Overview.

If the manifest enables MCP, the same operations are also exposed through the server’s /mcp endpoint.

How app invocation uses authorization

Gestalt resolves every app request to a canonical subject, then authorization decides whether that subject may use the app or operation.

When executable apps call other configured app operations, authorization scopes are threaded through the call stack for both users and service accounts.

Configuring an app provider

apps: github: displayName: GitHub source: package: github.com/valon-technologies/gestalt-providers/app/github version: 0.0.1-alpha.1 config: apiBaseUrl: https://api.github.com clientId: ${GITHUB_CLIENT_ID} clientSecret: secret: provider: default name: github-client-secret

During local development, point at a source manifest instead:

apps: support: source: ./apps/support/manifest.yaml

Hosted HTTP route overrides are deployment-specific; see Hosted HTTP endpoints.

Connections and credentials

The external credentials provider resolves credentials for configured connections. App manifests declare connection needs; deployment config binds or overrides them. Use mode: none or mode: subject depending on whether the connection should resolve credentials for the effective credential subject, including a caller, subject-owned API token owner, or runAs subject.

Auth types: oauth2, mcp_oauth, bearer, manual, none. See Config File > connections.

Filtering operations and egress

allowedOperations

Use allowedOperations to publish only a subset of a large app catalog:

apps: github: source: package: github.com/valon-technologies/gestalt-providers/app/github version: 0.0.1-alpha.1 allowedOperations: issues.list: alias: list_issues issues.get: alias: get_issue

allowedHosts

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

Use egress.allowedHosts when an app needs explicit outbound access:

apps: github: egress: allowedHosts: - api.github.com - "*.github.com"

For executable plugins, egress.allowedHosts is only a complete security boundary when the app runs inside a runtime that supports hostname-based egress controls.

Building your own app

Use this section for the app manifest and SDK implementation surface. For the shared package model, provider search/add commands, and release mechanics, see Provider packages and Releasing provider packages.

Manifest

You declare static metadata, connections, and passthrough surfaces in a manifest file. Gestalt accepts manifest.yaml, manifest.yml, or manifest.json.

kind: app source: github.com/your-org/apps/slack version: 0.0.1 displayName: Slack description: Send messages, search conversations, and manage channels. spec: mcp: true connections: default: auth: type: oauth2 authorizationUrl: https://slack.com/oauth/v2/authorize tokenUrl: https://slack.com/api/oauth.v2.access accessTokenPath: authed_user.access_token scopeParam: user_scope scopeSeparator: ',' scopes: - channels:read - chat:write - search:read - users:read

See Provider Manifests for the full field reference.

Creating a project

You need Go 1.26+ and a running gestaltd instance for testing.

mkdir slack && cd slack go mod init github.com/your-org/apps/slack go get github.com/valon-technologies/gestalt/sdk/go@latest

Recommended layout:

slack/ go.mod manifest.yaml provider.go

Defining operations

Operations are the callable units of an app. Each operation has an ID, an HTTP method, input and output types, and a handler function. Executable source apps can use SDK-native source package metadata, declare run for local source execution, or declare build.command and entrypoint.artifactPath when they need a compiled release artifact.

The following example defines a conversations.getMessage operation that fetches a single Slack message by channel and timestamp. The handler uses the resolved runtime token from the request context to call the Slack API.

package provider import ( "context" "net/http" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) type Provider struct{} type GetMessageInput struct { Channel string `json:"channel" doc:"Channel ID" required:"true"` TS string `json:"ts" doc:"Message timestamp" required:"true"` } type GetMessageOutput struct { Message map[string]any `json:"message"` } var ( getMessageOp = gestalt.Operation[GetMessageInput, GetMessageOutput]{ ID: "conversations.getMessage", Method: http.MethodGet, Description: "Get a single message from a Slack channel", } Router = gestalt.MustRouter( gestalt.Register(getMessageOp, (*Provider).getMessage), ) ) func New() *Provider { return &Provider{} } func (p *Provider) Configure(_ context.Context, _ string, _ map[string]any) error { return nil } func (p *Provider) getMessage( ctx context.Context, input GetMessageInput, req gestalt.Request, ) (gestalt.Response[GetMessageOutput], error) { // Call Slack API using req.Token as the resolved Bearer token. // Fetch conversations.history filtered to the single message at input.TS. return gestalt.OK(GetMessageOutput{Message: map[string]any{"ts": input.TS}}), nil }

By default, input decode failures return a structured 400 response, and unhandled errors return a structured 500 response.

Hosted HTTP endpoints

Hosted HTTP endpoints let an external product call an app through a stable HTTP route, while the app still handles the request as a normal operation. They are useful for webhooks and other inbound requests whose path, signature verification, body shape, or acknowledgment response needs to match the calling product.

The endpoint declaration belongs in the app manifest under spec.http and spec.securitySchemes. Deployment config selects the app and may override fields, but most deployments do not need a separate route declaration.

See Hosted HTTP Bindings for security schemes, request decoding, subject resolution, and target operations.

kind: app source: github.com/your-org/apps/slack version: 0.0.1 spec: securitySchemes: slack_signed: type: hmac secret: env: SLACK_SIGNING_SECRET signatureHeader: X-Slack-Signature signaturePrefix: v0= payloadTemplate: "v0:{header:X-Slack-Request-Timestamp}:{raw_body}" timestampHeader: X-Slack-Request-Timestamp maxAgeSeconds: 300 http: support_command: path: /commands/support method: POST credentialMode: none security: slack_signed target: handle_support_command requestBody: required: true content: application/x-www-form-urlencoded: {} ack: status: 200 body: status: accepted

Implement the target operation in the app SDK:

package provider import ( "context" "net/http" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) type Provider struct{} type SlackCommandInput struct { TeamID string `json:"team_id" required:"true"` UserID string `json:"user_id" required:"true"` ChannelID string `json:"channel_id" required:"true"` Text string `json:"text,omitempty"` } type SlackCommandOutput struct { OK bool `json:"ok"` } var ( supportCommandOp = gestalt.Operation[SlackCommandInput, SlackCommandOutput]{ ID: "handle_support_command", Method: http.MethodPost, } Router = gestalt.MustRouter( gestalt.Register(supportCommandOp, (*Provider).handleSupportCommand), ) ) func New() *Provider { return &Provider{} } func (p *Provider) Configure(_ context.Context, _ string, _ map[string]any) error { return nil } func (p *Provider) handleSupportCommand( ctx context.Context, input SlackCommandInput, req gestalt.Request, ) (gestalt.Response[SlackCommandOutput], error) { // Run the Slack command workflow. The host already verified the signature. return gestalt.OK(SlackCommandOutput{OK: true}), nil } func (p *Provider) ResolveHTTPSubject( ctx context.Context, req gestalt.HTTPSubjectRequest, ) (*gestalt.Subject, error) { teamID, _ := req.Params["team_id"].(string) userID, _ := req.Params["user_id"].(string) if teamID == "" || userID == "" { return nil, gestalt.Error(http.StatusBadRequest, "missing Slack subject fields") } return &gestalt.Subject{ ID: "slack:" + teamID + ":" + userID, Kind: "user", DisplayName: userID, AuthSource: "slack", }, nil }

Request query parameters are included in the operation input. JSON object bodies merge into the input by field name, form-encoded bodies become string parameters, and unsupported body types are passed as rawBody. If ack is configured, Gestalt sends that response immediately and runs the operation asynchronously. Without ack, the caller receives the operation response.

Declarative apps

Not every app needs executable code. If the upstream API is already reachable over HTTP, you can expose it as REST operations declared entirely in the manifest. Gestalt forwards each invocation to the upstream and resolves a runtime credential when the selected connection requires one. No provider process runs.

The following manifest exposes three Slack API endpoints as Gestalt operations, with no code at all:

kind: app source: github.com/your-org/apps/slack-readonly version: 0.0.1 displayName: Slack (read-only) description: Read Slack channels and messages via the REST API. spec: mcp: true surfaces: rest: baseUrl: https://slack.com operations: - name: conversations.list description: List conversations and channels method: GET path: /api/conversations.list parameters: - name: limit type: int in: query - name: types type: string in: query - name: conversations.history description: Read message history for a channel method: GET path: /api/conversations.history parameters: - name: channel type: string in: query required: true - name: limit type: int in: query - name: oldest type: string in: query - name: users.list description: List workspace users method: GET path: /api/users.list parameters: - name: limit type: int in: query connections: default: auth: type: oauth2 authorizationUrl: https://slack.com/oauth/v2/authorize tokenUrl: https://slack.com/api/oauth.v2.access accessTokenPath: authed_user.access_token scopeParam: user_scope scopeSeparator: ',' scopes: - channels:read - users:read

You configure a declarative app the same way as any other. Point the config at the manifest, and the declared operations appear in the catalog:

apps: slack-readonly: source: ./slack-readonly/manifest.yaml
gestalt app invoke slack-readonly conversations.list -p limit=5

Gestalt handles credential resolution, request formatting, and response passthrough. The spec.surfaces.rest.baseUrl is the root for all declared paths, and each operation’s parameters define how input fields map to query parameters or request body fields.

You can also declare OpenAPI, GraphQL, and MCP surfaces using spec.surfaces.openapi, spec.surfaces.graphql, and spec.surfaces.mcp. See Provider Manifests for the full surface schema.

Hybrid providers

An app can combine declarative passthrough surfaces with executable operations. The host forwards declarative operations directly to the upstream API, while executable operations run in the provider process. Both appear in the same catalog.

Do not define the same operation ID in more than one surface. If two surfaces expose the same ID, provider validation fails. Use allowedOperations to filter or alias collisions when needed.

Operation ID conventions

Operation IDs should use period-separated hierarchical names. The Slack app uses conversations.list, conversations.history, chat.postMessage, and users.info.

This convention enables prefix matching in the CLI. When you run gestalt app invoke slack conversations, the CLI joins the space-separated segments with periods and shows all operations whose ID starts with conversations.. This makes large catalogs navigable without memorizing every operation name.

Dynamic catalogs

By default, the operation catalog is static and materialized at startup. If your app needs to expose different operations depending on the caller’s credentials or runtime state, implement a session catalog.

A Slack app could use this to check which Slack workspace the caller is connected to and adjust the available operations accordingly.

func (p *Provider) CatalogForRequest(ctx context.Context, token string) (*gestalt.Catalog, error) { return &gestalt.Catalog{ Name: "slack-session", DisplayName: "Slack", Operations: []*gestalt.CatalogOperation{ { Id: "conversations.getMessage", Method: http.MethodGet, ReadOnly: true, }, }, }, nil }

Post-connect metadata

Apps can derive additional connection metadata after Gestalt completes a credential exchange. Use a post-connect hook when the upstream connect response contains workspace, tenant, or account identifiers that should be stored with the connection and reused by later operation requests. Return only non-secret metadata; Gestalt already stores access and refresh tokens separately.

import ( "context" "encoding/json" gestalt "github.com/valon-technologies/gestalt/sdk/go" ) func (p *Provider) PostConnect(ctx context.Context, token *gestalt.ConnectedToken) (map[string]string, error) { upstream := map[string]string{} if token.MetadataJSON != "" { _ = json.Unmarshal([]byte(token.MetadataJSON), &upstream) } return map[string]string{ "slack_team_id": upstream["team_id"], "subject_id": token.SubjectID, "connection": token.Connection, "instance": token.Instance, }, nil }

Testing locally

Use gestaltd provider validate and gestaltd serve --path when iterating on source apps.

Validate the app in isolation:

gestaltd provider validate --path ./slack

Run the app locally with browser-facing UI available:

gestaltd serve --path ./slack

Validate or serve the UI bundle directly when you are working on the frontend itself:

gestaltd provider validate --path ./ui gestaltd serve --path ./ui

When the app needs supporting providers, extra plugins, or explicit mounted UIs, layer real config overlays on top of the synthesized baseline:

gestaltd provider validate --path ./slack --config ./dev/support.yaml gestaltd serve --path ./slack --config ./dev/support.yaml

Repeated --config flags merge left-to-right, and null deletes inherited entries. That makes it possible to boot some supporting providers while suppressing others for a local session.

When your config already points at local provider source paths, use config-based serve with --watch to restart after local config, source provider, or local release metadata changes:

gestaltd serve \ --app slack \ --config ./base.yaml \ --config ./dev/local.yaml \ --watch

--watch is not supported with --path, --remote, or --locked; use it with --config files that reference the local sources you want to iterate on.

Attaching local code to a remote server

Remote local serve is under active development and is not yet stable. Behavior, authorization requirements, and attach-session semantics may change between releases without warning. Feedback and bug reports are welcome via GitHub Issues .

Use gestaltd serve --remote when you want a remote Gestalt instance to handle auth, authorization, connections, secrets, and host services, but route a selected app implementation to your local machine:

export GESTALT_URL=https://gestalt.example.com gestaltd serve --remote "$GESTALT_URL" --path ./slack

With --path and no --config, Gestalt matches the local manifest source to a configured remote app and preserves the remote app’s config. If more than one remote app uses the same source, or the manifest has no source, pass --name:

gestaltd serve --remote "$GESTALT_URL" --path ./slack --name slack

Use layered config files for explicit local config overrides or multi-app attach sessions:

gestaltd serve \ --remote "$GESTALT_URL" \ --config ./base.yaml \ --config ./prod.yaml \ --path ./slack \ --name slack

You can also attach every source-backed app from layered config overlays:

gestaltd serve \ --remote "$GESTALT_URL" \ --config ./base.yaml \ --config ./prod.yaml \ --config ./dev/local-apps.yaml

Remote attach is owner-scoped. The remote server must opt in with server.dev.attachmentState, and the target app must allow the caller’s role or API-token action grant. Other users keep seeing the deployed provider.

For attach setup, token permissions, diagnostics, and cleanup, see CLI and Config File > provider development.

Once the local server is running, invoke an operation:

gestalt app invoke slack conversations.getMessage -p channel=C01ABC23DEF -p ts=1712161829.000300

For application composition, see Applications. For configuration, see Configuration. For operation invocation, see HTTP API. For manifest fields, see Provider Manifests. For authorization behavior, see Authorization.