Skip to content

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

  1. Open Settings → Hosted plugins in Horizon.
  2. 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.
  3. Copy the generated command:
cs plugin host <source> --run
  1. Run it in your terminal. The command:
  2. finds the installed plugin and reads its hosted release metadata itself;
  3. builds or uses the plugin's read-only hosted server;
  4. creates a local TLS certificate and opens a temporary Cloudflare tunnel;
  5. registers the private endpoint with this workspace;
  6. keeps serving until you press Ctrl-C.
  7. 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:

  1. Update the installed plugin or pull the new plugin source.
  2. Run the same command again: cs plugin host <source> --run.
  3. The private listing and endpoint are replaced in place. Installations and audit history are kept.
  4. 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:

cs plugin update <source> && cs plugin host <source> --run

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:

<cloudslash_return>?cloudslash_setup=<base64url(JSON)>

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 --manifest mode, 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.
  • cloudflared for 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:

cs plugin host <source> --endpoint grpcs://<your-host>:443

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:

  1. Publish the exact release metadata on the registry Publish page.
  2. Set grpcs://<your-host>:443 as the hosted endpoint.
  3. Submit the manifest digest for operator approval.
  4. 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.