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 configuration 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.
Caller identity on request.subject
Executable app handlers receive the authenticated caller on
request.subject. For interactive users, request.subject.displayName is
populated from the platform session (typically the OIDC name claim). It is
not a directory lookup API. request.subject.email carries the resolved email
when the host knows it. Service accounts may populate displayName directly on
the subject id.
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, or body shape 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: {}Implement 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. The caller
receives the operation response when processing completes.
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
}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 \
--config ./base.yaml \
--config ./dev/slack-local.yaml \
--watch--watch is not supported with --path or --locked; use it with
--config files that reference the local sources you want to iterate on.
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 configuration.