Skip to main content
Tesslate OpenSail

Overview

In OpenSail, a deployment target is a first-class node on the Architecture Panel canvas. You draw a dashed orange edge from a container node (frontend, backend, worker) to a target node, and the orchestrator handles the rest: build, package, authenticate with the provider, and stream status back. The same container can fan out to many targets, each with its own environment, env-var overrides, deployment history, and rollback.

22 external targets

Serverless, static, container push, and registry export providers, all wired into the same canvas flow.

A/B variants

Drop a second target, point production at Vercel, preview at Cloudflare Pages, staging at Fly.io.

Per-target environment

Cycle production, staging, preview on the target header. Env-var merge runs in four layers.

Independent rollback

Every deployment row keeps its provider URL, logs, and metadata. Roll back any target without touching the others.

What a target owns

Each DeploymentTarget row tracks:
  • Provider (vercel, netlify, fly, and so on).
  • Environment label (production, staging, preview).
  • Env-var overrides in the deployment_env JSON column.
  • Deployment history with per-version rollback.
  • Live credential binding so the canvas node can render “Not Connected” when no credential exists yet.
Source references: orchestrator/app/models.py defines DeploymentTarget, DeploymentTargetConnection, Deployment, and DeploymentCredential. The canvas components are app/src/components/DeploymentTargetNode.tsx and app/src/components/edges/DeploymentEdge.tsx.

Provider catalog

OpenSail ships 22 targets grouped by deploy type. Classification lives in orchestrator/app/services/deployment/manager.py and is mirrored in app/src/components/DeploymentTargetNode.tsx.

Serverless and full-stack (11)

ProviderSlugAuthNotes
VercelvercelOAuthSource mode default, framework detection
NetlifynetlifyOAuthPre-built upload via Netlify Files API
Cloudflare Pages and WorkerscloudflareAPI tokenPre-built, account_id required
DigitalOcean App Platform (source)digitaloceanAPI tokenRequires git remote on project
RailwayrailwayAPI tokenRequires git remote
Fly.ioflyAPI tokenContainer push to Fly Machines
HerokuherokuAPI keySource tarball upload
RenderrenderAPI keyRequires git remote
KoyebkoyebAPI tokenServerless, optional org_slug
ZeaburzeaburAPI keyZIP upload, region optional
NorthflanknorthflankAPI tokenRequires git remote

Static hosting (4)

ProviderSlugAuthNotes
GitHub Pagesgithub-pagesGitHub PATrepo scope, static only
SurgesurgeEmail + tokenRun surge token in the CLI to fetch
Deno Deploydeno-deployAccess tokenorg_id required
Firebase HostingfirebaseService-account JSONsite_id required

Container push (4)

ProviderSlugAuthNotes
AWS App Runneraws-apprunnerAccess key + secretPushes to ECR, then App Runner
GCP Cloud Rungcp-cloudrunService-account JSONPushes to Artifact Registry
Azure Container Appsazure-container-appsTenant, client, secret, subscriptionPushes to ACR
DigitalOcean Container Appsdo-containerAPI tokenPushes to DOCR

Registry and export (3)

ProviderSlugAuthNotes
Docker HubdockerhubUsername + PATRegistry push only, no runtime
GitHub Container RegistryghcrUsername + PAT (write:packages)Registry push only
Download and ExportdownloadNoneZip archive delivered to the browser

Connecting a provider

Open Settings > Deployments (app/src/pages/settings/DeploymentSettings.tsx). Each provider card reads its requirements from /api/deployment-credentials/providers served by DeploymentManager.list_available_providers().
Vercel and Netlify use OAuth. Click Connect on the provider card and a popup kicks off the authorize route:
  • /api/deployment-oauth/vercel/authorize
  • /api/deployment-oauth/netlify/authorize
The callback handler in orchestrator/app/routers/deployment_oauth.py exchanges the code, encrypts the token with the Fernet key in deployment_encryption.py, and persists a DeploymentCredential row. Vercel uses PKCE; pass team_id in metadata for team accounts.
Rotate a credential by reconnecting from the same Settings card. The old row is updated in place, so any targets already bound to it stay connected.

Architecture Panel usage

1

Open the Architecture Panel

Open your project, switch to the Architecture tab, and confirm your containers are visible.
2

Drop a Deployment Target

Drag a Deployment Target from the node palette, pick a provider from the list of 22, and drop it on the canvas. The orchestrator creates a DeploymentTarget row via POST /api/projects/{slug}/deployment-targets.
3

Draw the deployment edge

Connect a container node’s right handle to the target node’s left handle. The edge POSTs to /api/projects/{slug}/deployment-targets/{target_id}/connect/{container_id}, which inserts a DeploymentTargetConnection row.
4

Cycle the environment

Click the environment label (production, staging, preview) on the target header to cycle values. This writes back to DeploymentTarget.environment.
5

Edit overrides

Double-click the target (or use the card menu) to set env-var overrides, a custom build command, or a display name. Per-edge overrides live in DeploymentTargetConnection.deployment_settings.
6

Inspect history

The Deployment History panel on the node pulls the last 10 rows from /deployment-targets/{target_id}/history.
For a full walkthrough of the canvas, see the Architecture Panel guide.

Triggering a deploy

Three entry points, one pipeline:

Target node Deploy button

POST /api/projects/{slug}/deployment-targets/{target_id}/deploy. Loops over every connected container in one request. This is the standard path.

Deploy modal

POST /api/deployments/{project_slug}/deploy with provider, deployment_mode, env_vars, and custom_domain. Powers the top-right Deploy button.

Agent tool

The agent can deploy on your behalf. See the agent tool reference.

What happens server-side

When a deploy fires in source or file mode, routers/deployments.py::deploy_project (or routers/deployment_targets.py::deploy_target) runs this pipeline:
  1. RBAC check via get_project_with_access(Permission.DEPLOYMENT_CREATE).
  2. Resolve the credential with project-override precedence.
  3. Decrypt the token with get_deployment_encryption_service().
  4. Insert a Deployment row with status="building" so the UI shows progress.
  5. Pick the primary build container and ensure it is running.
  6. If deployment_mode == "source", skip the local build (the provider builds remotely). Otherwise invoke get_deployment_builder().trigger_build() which runs npm run build (or the container-specific build) inside the live container.
  7. Collect files with builder.collect_deployment_files(), honoring container.output_directory.
  8. Hand files to the provider via DeploymentManager.get_provider(name, creds). Source-mode providers call provider.deploy(files, config). Container-push providers go through provider.push_image() then provider.deploy_image().
  9. Update the Deployment row with status, deployment_url, logs, completed_at, and deployment_metadata.
  10. Poll or stream provider status where supported (Vercel, Netlify, cloud providers). Otherwise the initial response carries the final state.
Deployment tokens never leave the server. The frontend only receives deployment_url, status, and logs.

A/B deploys

Because each target is an independent node, one container can connect to many targets. Drop a second Vercel target set to preview and a third Netlify target set to staging, then wire the same frontend container into all three. Each target keeps its own Deployment history, so URLs, rollback, and env-var overrides stay isolated. Recommended patterns:
  • Split traffic between Vercel (production) and Cloudflare Pages (preview).
  • Deploy the same static build to GitHub Pages and Surge for redundancy.
  • Keep staging.myapp.com on Fly.io while production runs on AWS App Runner.

Environment matrix

Env vars flow through a four-layer merge in routers/deployment_targets.py::_build_source_env_vars. Later layers win.
LayerSourceTypical use
1. Auto-derivedproject.git_remote_url becomes _TESSLATE_REPO_URLGit-required providers
2. Target-levelDeploymentTarget.deployment_env JSONValues shared by every container on the target (NODE_ENV=production)
3. Connection-levelDeploymentTargetConnection.deployment_settings["env_vars"]Per-container differences against the same target
4. Request-levelenv_vars passed in the deploy callOne-off overrides from the modal or agent
Internal keys prefixed with _TESSLATE_ (see services/deployment/base.py, INTERNAL_ENV_PREFIX) are stripped before anything hits the provider. The environment column is metadata only, so set NODE_ENV or VERCEL_ENV yourself on the target’s deployment_env if your runtime needs it.

Per-provider caveats

Defaults to source mode so Vercel builds on its own infrastructure. Pass team_id in metadata for team accounts. OAuth flows through /api/deployment-oauth/vercel/authorize with PKCE.
The file-upload API does not trigger a remote build, so OpenSail defaults to pre-built. The local build must complete before the upload starts.
pre-built only. The provider upload uses the dispatch namespace in metadata if set. account_id is mandatory.
Deploys to Fly Machines, not the legacy Nomad stack. Set org_slug for multi-org accounts. Volumes are not auto-created, so declare them in the container’s fly.toml.
Requires ECR access. The provider pushes the image to a per-region ECR repository using the supplied IAM key, then creates an App Runner service pointing at the pushed tag.
Static only. The token needs the repo scope. The provider publishes to the gh-pages branch.
Registry only. No runtime, no URL, no health checks. The button label switches to “Export”.
Bundles the build output as a zip and returns a signed URL. No credentials needed. Great for offline handoffs.
Two slugs share the same backend class. digitalocean is the source-mode alias (git deploy). do-container is the image-push alias (DOCR + App Platform container spec).

Rolling back

From the target node’s deployment history, click Rollback on any prior successful row. The UI calls:
POST /api/projects/{slug}/deployment-targets/{target_id}/rollback/{deployment_id}
The orchestrator creates a new Deployment row with version="{prev}-rollback", linked via deployment_metadata.rollback_from. Full provider-API rollback (Vercel alias swap, Fly release pin) is tracked in the same handler and rolls forward as each provider implements it.

Credential security

Fernet at rest

Every access_token_encrypted column is Fernet-encrypted with the backend key. Decryption happens only inside the deployment pipeline, never in the API response.

Team-scoped defaults

Credentials are scoped to user_id. Add project_id for an override that only applies to that project (useful for team members with different Vercel accounts).

Token never returned

GET /api/deployment-credentials returns metadata and the is_default flag. The token field is never serialized back to the client.

In-place rotation

Reconnect from Settings. Revoked tokens start returning 401 from the provider. The next credential sync flips the target node back to “Not Connected”.
provider_metadata JSON stores team IDs, registry names, and regions in plaintext. Do not put secondary secrets there. Always use the encrypted token field.

Common errors

No DeploymentCredential exists for this user and project. Connect the provider in Settings > Deployments, or pass project_id when creating the credential for a project-scoped override.
The target exists on the canvas but credential lookup returned None. The node shows a yellow “Not Connected” banner until a credential is bound. Reconnect from the same Settings card.
The orchestrator cannot exec a build in a stopped container. Start the project from the canvas, wait for green status, then retry. Source-mode providers (Vercel, DigitalOcean) skip the local build step and do not hit this error.
The container build command failed. Check container.build_command, install missing dependencies, and read the logs attached to the Deployment row. For long builds, split the project into smaller containers or switch to source mode.
Git-required providers (railway, render, digitalocean, github-pages, northflank) need a git remote on the project. Link a repo via the Git panel and retry.
The token expired or its scope was revoked from the provider side. Reconnect the provider in Settings and the next credential sync flips the target node back to green.
The desktop sidecar tried to hit cloud providers. Deploy from cloud OpenSail instead; desktop only runs local and docker runtimes.
Free-tier limits hit (projects, bandwidth, build minutes). Upgrade the provider plan or deploy to a second target of a different provider.
A long build blocks the sync request. Split into smaller containers, or use source mode where the provider builds remotely and OpenSail only uploads code.

Where to next

Publish an app

Turn a deployed project into a distributable Tesslate App with versioning, approval, and billing.

Communication gateways

Wire a deployed backend to Telegram, Slack, Discord, or WhatsApp via the gateway.

Architecture Panel

Learn the full canvas: containers, connections, env injection, and deployment edges.