Self-host plugins¶
Any v3 source plugin can run where you run your infrastructure. This is not provider-specific: a plugin for a database, Kubernetes, a SaaS API, a cloud, or an internal system uses the same contract.
Two ways to self-host¶
| Mode | Who can see it | Who approves it | Best for |
|---|---|---|---|
| Private workspace hosting | Only your organization | Your workspace | CLI-only plugins you want to run in Horizon |
| Public hosting | Everyone | CloudSlash operator | A publisher offering their plugin as a service |
Private hosting is the fast path. It does not publish anything to the registry and does not require operator approval.
Host a CLI-only plugin in your workspace¶
- Open Settings → Hosted plugins in Horizon.
- Choose one of the sources already detected in your workspace (any system, not just a cloud). The list comes from your scans and installed plugins.
- Copy the generated command:
- Run it in your terminal. The command:
- finds the installed plugin and reads its hosted release metadata itself;
- builds or uses the plugin's read-only hosted server;
- creates a local TLS certificate and opens a temporary Cloudflare tunnel;
- registers the private endpoint with this workspace;
- keeps serving until you press
Ctrl-C. - In Horizon, open Sources and add a connection for the hosted plugin.
You never paste a URL or JSON into the browser. The command is generic: the plugin decides its own connection fields, and the source can be AWS, GCP, Azure, Kubernetes, PostgreSQL, Git, SaaS, bare metal, or anything else a plugin supports.
The plugin now appears under Hosted for this workspace and in Sources.
It is visible only to that organization. Its internal name is namespaced to the
workspace, so it can never collide with the public plugin or another workspace.
Hosting inserts a normal org_plugin_installations row with mode hosted, so
the plugin counts as installed for the workspace plugin limit and appears in
the Plugins page, Sources, Connections and Hosted plugins. It is not a separate
kind of plugin and it never enters the public catalog.
Hard boundary
A private listing can never be promoted to a public listing, and hosting a plugin privately never edits the publisher's public catalog. Only the plugin's original publisher can publish a public release.
Updating a private release¶
When the publisher ships a new version:
- Update the installed plugin or pull the new plugin source.
- Run the same command again:
cs plugin host <source> --run. - The private listing and endpoint are replaced in place. Installations and audit history are kept.
- Existing connections are marked invalid until they are re-verified, because the endpoint or capability schema may have changed.
When the public catalog ships a newer hosted release, Settings → Hosted plugins shows the new version and a copyable update command:
The hosted build takes its version from the installed plugin manifest. A publisher bumps the manifest once; every self-hosted build and every connection then reports that same version. No version is hardcoded in the hosted server.
A newer CLI-only release does not supersede a private hosted runtime until the workspace rebuilds it. The update hint is the prompt to do that.
Plugin-owned setup pages¶
A connection method can declare an HTTPS setup_url. When it does, Horizon
does not present a generic credential form first. The client opens the plugin's
own setup page and passes:
cloudslash_state— a one-time, 15-minute setup session bound to this workspace, plugin, connection method and reviewed release digest;cloudslash_return— the workspace URL to return to.
The plugin does the provider-specific work (OAuth consent, role creation, database credential creation, SSH key registration, or anything else) and then returns the finished connection to:
The JSON is:
{
"state": "<cloudslash_state>",
"display_name": "Production account",
"configuration": { "role_arn": "arn:...", "region": "us-east-1" },
"granted_permissions": ["sts:AssumeRole", "ec2:Describe*"]
}
Horizon validates the one-time session, the reviewed release digest, the method's configuration schema and the selected permissions, then creates the connection automatically. The customer only approves inside the plugin's own setup page. The session is single-use; a replayed return is rejected.
A plugin that runs setup inside a provider account (for example a CloudFormation custom resource) can call the same completion server-to-server instead of redirecting the browser:
POST https://api.cloudslash.dev/v1/connections/setup-plugin-callback
Content-Type: application/json
{
"state": "<cloudslash_state>",
"display_name": "Production account",
"configuration": { "...": "..." },
"granted_permissions": ["read"]
}
The unguessable one-time state is the authority for this call; no Clerk token is available inside the customer's provider account. The state is bound to the workspace, plugin, method and reviewed release digest and expires after 15 minutes.
A setup_url may contain the tokens CLOUDSLASH_STATE and
CLOUDSLASH_RETURN. Horizon substitutes the one-time values before opening the
URL; otherwise it appends cloudslash_state and cloudslash_return as query
parameters. This lets a plugin open a provider's prefilled quick-create page
directly, without an intermediate CloudSlash page.
The bundled AWS plugin uses this path. Its setup_url points straight at a
prefilled CloudFormation review page. The template is hosted on S3 because
CloudFormation only accepts an S3 TemplateURL. The stack creates the read-only
role and a small Lambda custom resource calls the setup callback with the role
ARN. The customer reviews the stack and clicks Create; no ARN or key is
pasted, and the connection appears in Sources. See
plugins/cloudslash-plugin-aws/setup/README.md and
plugins/cloudslash-plugin-aws/setup/cloudformation.yaml for the reference
implementation.
The stack and every resource it creates live in the self-hoster's AWS account. The publisher's S3 bucket only hosts the public template. CloudSlash never uses publisher credentials to create resources in a customer account; it only opens the setup URL and receives the one-time callback. The same boundary applies to every provider: GCP creates in the customer's project, Azure in the customer's subscription, Kubernetes in the customer's cluster, PostgreSQL in the customer's database. The plugin author owns the provider template; the customer owns the account and approves it there.
Plugins without a setup_url use the ordinary field form. Plugins with a
setup_url keep that form under Advanced as a fallback. The setup page is
authored by the plugin, so it can look like anything the provider requires; it
is never hardcoded as IAM, SSH, a cloud console, or an API token.
What you need¶
- A plugin whose publisher ships a v3 source (the source contains
cmd/hosted, a--manifestmode, or an equivalent server entrypoint). See Build a plugin → Hosted entrypoint for the SDK contract, connection methods, setup pages and version injection. - A host with outbound read access to the target system.
- A read-only credential/role for that target.
cloudflaredfor the one-command temporary endpoint, or your own public TLS endpoint passed with--endpoint grpcs://<your-host>:443.
A legacy CLI-only plugin that never exposes a hosted protocol cannot serve
hosted scans. You can still run it locally with cs scan; ask the publisher for
a v3 source build before hosting it.
Manual build (advanced)¶
cs plugin host <source> --run handles this automatically when it can find the
plugin source. To run the server yourself, use a stable public TLS endpoint:
# in the plugin source directory
go build -o cloudslash-plugin ./cmd/hosted
./cloudslash-plugin --manifest
LISTEN_ADDR=0.0.0.0:8443 \
TLS_CERT_FILE=/certs/tls.crt \
TLS_KEY_FILE=/certs/tls.key \
HORIZON_PUBLIC_KEY=<32-byte-ed25519-public-key-hex> \
PLUGIN_NAME=<namespaced workspace listing> \
./cloudslash-plugin
Then register the endpoint:
Terminate TLS at your ingress and expose grpcs://<your-host>:443. Do not
expose the plugin on plaintext gRPC.
Public hosting (publishers only)¶
If you are the original publisher and want the plugin available to everyone:
- Publish the exact release metadata on the registry Publish page.
- Set
grpcs://<your-host>:443as the hosted endpoint. - Submit the manifest digest for operator approval.
- The release becomes installable by any workspace once approved.
Public hosted releases require operator approval. Private workspace releases do not: your workspace is making its own trust decision about an endpoint it operates.
A generic container¶
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/plugin ./cmd/hosted
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/plugin /plugin
ENV LISTEN_ADDR=0.0.0.0:8443
EXPOSE 8443
ENTRYPOINT ["/plugin"]
Mount the TLS certificate and key at runtime. The plugin does not read credentials from the image.
If the publisher hosts it¶
If the registry shows a hosted endpoint, you can use the publisher's runner
without deploying anything. If it does not, you are not blocked: host it
privately for your workspace, self-host the source in your own VPC, or run it
locally with cs scan and cs sync.
Donations¶
A publisher may attach any HTTPS support/donation link. CloudSlash does not process, split, verify, or guarantee those payments. Sending 0 is always fine; the plugin remains free to use. Donations go directly to the publisher.