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-secretDuring local development, point at a source manifest instead:
apps:
support:
source: ./apps/support/manifest.yamlHosted 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_issueallowedHosts
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.
YAML
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:readSee Provider Manifests for the full field reference.
Creating a project
Go
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@latestRecommended layout:
slack/
go.mod
manifest.yaml
provider.goDefining 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.
Go
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: acceptedImplement the target operation in the app SDK:
Go
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:readYou 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.yamlgestalt app invoke slack-readonly conversations.list -p limit=5Gestalt 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.
Go
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.
Go
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 ./slackRun the app locally with browser-facing UI available:
gestaltd serve --path ./slackValidate or serve the UI bundle directly when you are working on the frontend itself:
gestaltd provider validate --path ./ui
gestaltd serve --path ./uiWhen 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.yamlRepeated --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
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 ./slackWith --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 slackUse 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 slackYou 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.yamlRemote 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.000300What to read next
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.