# Authentication Source: https://docs.tesslate.com/authentication JWT sessions, OAuth providers, email 2FA, external API keys, and desktop pairing for OpenSail Tesslate OpenSail ## Overview OpenSail supports five authentication paths, all enforced by the orchestrator: 1. Password (bcrypt) with optional email 2FA 2. OAuth login (GitHub, Google) 3. OAuth connection for third-party services (deployments, git providers, MCP platform apps) 4. External API keys (`tsk_*`) for the external agent API and gateway 5. Desktop pairing (`tsk_*` exchanged via deep link and stored in Stronghold) Sessions use short-lived JWT access tokens with refresh token rotation. CSRF protection is a double-submit cookie pattern. Secrets at rest (OAuth tokens, channel credentials, deployment credentials) are encrypted with Fernet. ## Password and email 2FA `POST /api/auth/register` with `email`, `username`, `password`. The orchestrator hashes the password with bcrypt and returns an access token. The first account on a fresh install is promoted to admin. `POST /api/auth/login` with username or email and password. Returns an access token (default 30 min) and sets a refresh cookie (default 14 days). When `TWO_FA_ENABLED=true`, login triggers `POST /api/auth/2fa/send` which emails a 6-digit code. Submit with `POST /api/auth/2fa/verify`. Codes are Argon2 hashed, limited to 5 attempts, and expire in minutes. Access tokens renew automatically via the refresh cookie. No explicit refresh call is needed from the client. Required environment variables for email flows: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_SENDER_EMAIL`, `SMTP_USE_TLS`. ## OAuth login providers Two providers ship out of the box. Both are optional; absence of credentials disables the provider gracefully. Scopes: `user:email`, `read:user`. | Variable | Purpose | | --------------------------- | ------------------------------------------------------------- | | `GITHUB_CLIENT_ID` | Client ID from github.com/settings/developers | | `GITHUB_CLIENT_SECRET` | Client secret | | `GITHUB_OAUTH_REDIRECT_URI` | Must match the OAuth app exactly, including protocol and path | Local dev callback: `http://localhost/api/auth/github/callback`. Scopes: `openid`, `email`, `profile`. | Variable | Purpose | | --------------------------- | ------------------------------------ | | `GOOGLE_CLIENT_ID` | OAuth 2.0 client ID | | `GOOGLE_CLIENT_SECRET` | Client secret | | `GOOGLE_OAUTH_REDIRECT_URI` | Must match the Cloud Console setting | Local dev callback: `http://localhost/api/auth/google/callback`. Multiple providers can link to the same account. If the OAuth email matches an existing user, providers attach automatically. ## OAuth for deployment targets and git Connect providers for push-based deployments and private repo import. Configure client IDs and secrets under `Settings -> Integrations`. Tokens are Fernet-encrypted before being stored in `DeploymentCredential`. | Provider | Env variables | | ------------ | ----------------------------------------------------------------------------------------- | | Vercel | `VERCEL_CLIENT_ID`, `VERCEL_CLIENT_SECRET`, `VERCEL_OAUTH_REDIRECT_URI` | | Netlify | `NETLIFY_CLIENT_ID`, `NETLIFY_CLIENT_SECRET`, `NETLIFY_OAUTH_REDIRECT_URI` | | Heroku | `HEROKU_CLIENT_ID`, `HEROKU_CLIENT_SECRET`, `HEROKU_OAUTH_REDIRECT_URI` | | DigitalOcean | `DIGITALOCEAN_CLIENT_ID`, `DIGITALOCEAN_CLIENT_SECRET`, `DIGITALOCEAN_OAUTH_REDIRECT_URI` | Git providers (GitHub, GitLab, Bitbucket) are optional. Without them you can still import public repos. ## External API keys The external agent API lets machine users invoke agents with a bearer token. Keys are minted from `Settings -> API Keys` (`POST /api/external-api-keys`). They are SHA-256 hashed before storage (`ExternalAPIKey`). Only the newly minted secret is ever shown. Visit `Settings -> API Keys` and click **Create key**. Name it, pick scope (project-scoped or team-scoped), and copy the `tsk_*` secret immediately. `POST /api/external/agent/invoke` with `Authorization: Bearer tsk_...`. Returns `task_id` and `events_url` right away. Subscribe to `GET /api/external/agent/events/{task_id}` (SSE) or poll `GET /api/external/agent/status/{task_id}`. Delete the key in the UI or `DELETE /api/external-api-keys/{id}`. The hash is wiped; in-flight tasks abort on their next iteration. See the [external agent API guide](/guides/api-keys) for scopes, rate limits, and webhook callbacks. ## Desktop pairing The desktop app can pair to a cloud instance (tesslate.com or your own self-hosted cluster). Pairing unlocks the marketplace, bidirectional sync, and the remote K8s runtime. In the cloud instance, `Settings -> API Keys -> Create desktop key`. This returns a short-lived deep-link URL (`opensail://pair?token=...`). Click the URL on the machine running the desktop app. The Tauri deep-link handler exchanges the token for a long-lived `tsk_*` key. The key is written to the Tauri Stronghold vault (encrypted on disk with a per-machine passphrase). The sidecar never sees the plaintext key at rest; it is injected into the per-request Authorization header by the Rust shell. `GET /api/desktop/session/ping` confirms pairing. The tray icon turns green. Every cloud call made by the desktop app uses this key. Unpair by deleting it in the desktop `Settings` page, which also revokes it on the cloud side. ## Session management Access tokens default to 30 minutes (`ACCESS_TOKEN_EXPIRE_MINUTES`). Refresh tokens default to 14 days (`REFRESH_TOKEN_EXPIRE_DAYS`). Refresh is automatic while the browser tab is open. Production sets `COOKIE_SECURE=true`, `COOKIE_SAMESITE=lax`, and `COOKIE_DOMAIN=.your-apex.tld` so subdomain project URLs authenticate cleanly. `POST /api/auth/logout` clears the refresh cookie and invalidates the server-side refresh record. ## Security model | Concern | Mechanism | | ---------------- | ------------------------------------------------------------------------------------------------------------- | | Password storage | bcrypt with per-user salt | | 2FA codes | Argon2 hashed, 5-attempt cap, minute TTL | | Refresh rotation | One-time-use refresh tokens, detected reuse invalidates the session | | Secrets at rest | Fernet (`CHANNEL_ENCRYPTION_KEY`, `DEPLOYMENT_ENCRYPTION_KEY`, falls back to a key derived from `SECRET_KEY`) | | CSRF | Double-submit cookie pattern with `X-CSRF-Token` header | | OAuth state | Signed, single-use state token; mismatches abort the flow | | API keys | `tsk_*` prefix, SHA-256 hashed in DB, shown once at creation | | Desktop pairing | Short-lived deep-link token; long-lived key lives in Stronghold | Rotating `SECRET_KEY` invalidates existing JWTs and may make Fernet-encrypted data unrecoverable unless you also rotate `CHANNEL_ENCRYPTION_KEY` and `DEPLOYMENT_ENCRYPTION_KEY` in sync. ## Troubleshooting Click **Forgot Password** on login, enter your email, and follow the link. Resets require a working SMTP configuration. Check the caps-lock state, try username instead of email, and check the email inbox (and spam) for a 2FA code if `TWO_FA_ENABLED=true`. Browser cache flushes often resolve stale cookies. The OAuth app's callback URL must exactly match `*_OAUTH_REDIRECT_URI`, including protocol (`http` vs `https`) and path. Just log in again. Data is safe; sessions can expire after extended inactivity. Check that `COOKIE_DOMAIN` covers the subdomain you are on in production. Verify the deep-link scheme (`opensail://`) is registered on the OS. On Windows, reinstall with admin rights if the registration is missing. The token expires in minutes; mint a fresh one. ## Next steps Mint keys, invoke the external agent API, set scopes. Install the desktop app and pair it to your cloud. OAuth, SMTP, and cookie settings for self-hosted. Full security overview in the architecture page. # API Endpoints Source: https://docs.tesslate.com/developer/api-reference Complete reference of all backend REST API endpoints ## Overview OpenSail exposes a REST API through FastAPI. All endpoints are served from the Orchestrator backend. Authenticated endpoints require a valid JWT token (via `Authorization: Bearer {token}` header) or an HTTP-only session cookie. This reference organizes every endpoint by router. For each endpoint, you will find the HTTP method, path, a brief description, and whether authentication is required. Visit `http://localhost:8000/docs` (local) or `https://your-domain/docs` (production) to access the interactive Swagger UI, which includes live request testing and schema documentation. ## Auth Endpoints Authentication, registration, and session management. | Method | Path | Description | Auth | | ------ | ----------------------------- | ------------------------------------------- | ---- | | POST | `/api/auth/register` | Register a new user (email + password) | No | | POST | `/api/auth/login` | Login with email and password (returns JWT) | No | | POST | `/api/auth/logout` | Logout and invalidate session | Yes | | POST | `/api/auth/forgot-password` | Request password reset email | No | | POST | `/api/auth/reset-password` | Reset password using token from email | No | | POST | `/api/auth/verify` | Verify email address | No | | GET | `/api/auth/google/authorize` | Initiate Google OAuth login | No | | GET | `/api/auth/google/callback` | Google OAuth callback handler | No | | GET | `/api/auth/github/authorize` | Initiate GitHub OAuth login | No | | GET | `/api/auth/github/callback` | GitHub OAuth callback handler | No | | POST | `/api/auth/2fa/verify` | Verify 2FA email code | No | | POST | `/api/auth/2fa/resend` | Resend 2FA verification code | No | | POST | `/api/auth/verify-pod-access` | Verify pod access token (K8s internal) | No | ## User Endpoints User profile management. | Method | Path | Description | Auth | | ------ | ----------------- | --------------------------- | ---- | | GET | `/api/users/me` | Get current user profile | Yes | | PATCH | `/api/users/me` | Update current user profile | Yes | | GET | `/api/users/{id}` | Get user public profile | No | ## Project Endpoints Project CRUD, file operations, container management, and assets. ### Project Management | Method | Path | Description | Auth | | ------ | --------------------------- | ---------------------------- | ---- | | GET | `/api/projects` | List user's projects | Yes | | POST | `/api/projects` | Create a new project | Yes | | GET | `/api/projects/{id}` | Get project details | Yes | | PATCH | `/api/projects/{id}` | Update project metadata | Yes | | DELETE | `/api/projects/{id}` | Delete a project | Yes | | GET | `/api/projects/{id}/status` | Get project container status | Yes | ### Container Lifecycle | Method | Path | Description | Auth | | ------ | ---------------------------------------------------- | ------------------------------ | ---- | | POST | `/api/projects/{id}/start` | Start project containers | Yes | | POST | `/api/projects/{id}/stop` | Stop project containers | Yes | | POST | `/api/projects/{id}/restart` | Restart project containers | Yes | | POST | `/api/projects/{id}/containers` | Add a container to the project | Yes | | DELETE | `/api/projects/{id}/containers/{container_id}` | Remove a container | Yes | | PATCH | `/api/projects/{id}/containers/{container_id}` | Update container config | Yes | | POST | `/api/projects/{id}/containers/{container_id}/start` | Start a single container | Yes | | POST | `/api/projects/{id}/containers/{container_id}/stop` | Stop a single container | Yes | ### File Operations | Method | Path | Description | Auth | | ------ | ------------------------------------ | ---------------------- | ---- | | GET | `/api/projects/{id}/files` | List all project files | Yes | | GET | `/api/projects/{id}/files/{path}` | Read a file | Yes | | PUT | `/api/projects/{id}/files/{path}` | Write/update a file | Yes | | DELETE | `/api/projects/{id}/files/{path}` | Delete a file | Yes | | POST | `/api/projects/{id}/files/rename` | Rename/move a file | Yes | | POST | `/api/projects/{id}/files/directory` | Create a directory | Yes | ### Assets | Method | Path | Description | Auth | | ------ | -------------------------------------- | ------------------- | ---- | | GET | `/api/projects/{id}/assets` | List project assets | Yes | | POST | `/api/projects/{id}/assets` | Upload an asset | Yes | | DELETE | `/api/projects/{id}/assets/{asset_id}` | Delete an asset | Yes | ### Connections and Previews | Method | Path | Description | Auth | | ------ | ------------------------------------------ | ----------------------------- | ---- | | GET | `/api/projects/{id}/connections` | List container connections | Yes | | POST | `/api/projects/{id}/connections` | Create a container connection | Yes | | DELETE | `/api/projects/{id}/connections/{conn_id}` | Delete a connection | Yes | | GET | `/api/projects/{id}/previews` | List browser previews | Yes | | POST | `/api/projects/{id}/previews` | Create a browser preview | Yes | | DELETE | `/api/projects/{id}/previews/{preview_id}` | Delete a preview | Yes | ### Snapshots | Method | Path | Description | Auth | | ------ | ---------------------------------------------------- | --------------------------------- | ---- | | GET | `/api/projects/{id}/snapshots` | List project snapshots (timeline) | Yes | | POST | `/api/projects/{id}/snapshots` | Create a manual snapshot | Yes | | POST | `/api/projects/{id}/snapshots/{snapshot_id}/restore` | Restore from snapshot | Yes | ## Chat Endpoints AI agent chat management and streaming. | Method | Path | Description | Auth | | ------ | -------------------------------- | --------------------------------- | ---- | | GET | `/api/chat/history/{project_id}` | Get chat history for a project | Yes | | GET | `/api/chat/{chat_id}` | Get a specific chat with messages | Yes | | POST | `/api/chat/stream` | Stream agent response (SSE) | Yes | | POST | `/api/chat/cancel` | Cancel an in-progress agent run | Yes | | DELETE | `/api/chat/{chat_id}` | Delete a chat | Yes | | WS | `/api/chat/ws/{chat_id}` | WebSocket for real-time chat | Yes | ## Git Endpoints Git operations executed inside project containers. | Method | Path | Description | Auth | | ------ | -------------------------------- | ------------------------------- | ---- | | POST | `/api/git/init` | Initialize a Git repository | Yes | | POST | `/api/git/clone` | Clone a repository into project | Yes | | POST | `/api/git/commit` | Stage and commit changes | Yes | | POST | `/api/git/push` | Push commits to remote | Yes | | POST | `/api/git/pull` | Pull changes from remote | Yes | | GET | `/api/git/status/{project_id}` | Get Git status (changed files) | Yes | | GET | `/api/git/branches/{project_id}` | List branches | Yes | | POST | `/api/git/checkout` | Checkout a branch | Yes | ### Git Provider Endpoints Multi-provider Git integration (GitHub, GitLab, Bitbucket). | Method | Path | Description | Auth | | ------ | ------------------------------------------ | ------------------------------- | ---- | | GET | `/api/git-providers/providers` | List connected Git providers | Yes | | GET | `/api/git-providers/{provider}/repos` | List repositories from provider | Yes | | POST | `/api/git-providers/{provider}/connect` | Connect a Git provider (OAuth) | Yes | | DELETE | `/api/git-providers/{provider}/disconnect` | Disconnect a Git provider | Yes | ## Deployment Endpoints External deployments to Vercel, Netlify, and Cloudflare. | Method | Path | Description | Auth | | ------ | ----------------------------------------- | ---------------------------------- | ---- | | GET | `/api/deployments/{project_id}` | List deployments for a project | Yes | | POST | `/api/deployments` | Create a new deployment | Yes | | POST | `/api/deployments/deploy-all` | Deploy all containers in a project | Yes | | GET | `/api/deployments/{deployment_id}/status` | Get deployment status | Yes | | DELETE | `/api/deployments/{deployment_id}` | Delete a deployment record | Yes | ### Deployment Credentials | Method | Path | Description | Auth | | ------ | ---------------------------------- | ----------------------------------- | ---- | | GET | `/api/deployment-credentials` | List connected deployment providers | Yes | | POST | `/api/deployment-credentials` | Store deployment credentials | Yes | | DELETE | `/api/deployment-credentials/{id}` | Remove deployment credentials | Yes | ### Deployment OAuth | Method | Path | Description | Auth | | ------ | -------------------------------------------- | -------------------------------------- | ---- | | GET | `/api/deployment-oauth/{provider}/authorize` | Initiate OAuth for deployment provider | Yes | | GET | `/api/deployment-oauth/{provider}/callback` | OAuth callback handler | Yes | ## Marketplace Endpoints Browse, purchase, and manage AI agents and project templates. ### Agents | Method | Path | Description | Auth | | ------ | --------------------------------------- | ------------------------- | -------- | | GET | `/api/marketplace/agents` | Browse marketplace agents | Optional | | GET | `/api/marketplace/agents/{slug}` | Get agent details | Optional | | POST | `/api/marketplace/agents` | Publish a new agent | Yes | | PATCH | `/api/marketplace/agents/{id}` | Update an agent | Yes | | DELETE | `/api/marketplace/agents/{id}` | Delete an agent | Yes | | POST | `/api/marketplace/agents/{id}/purchase` | Purchase an agent | Yes | | POST | `/api/marketplace/agents/{id}/fork` | Fork an agent | Yes | | GET | `/api/marketplace/agents/{id}/reviews` | Get agent reviews | Optional | | POST | `/api/marketplace/agents/{id}/reviews` | Submit a review | Yes | ### Bases (Project Templates) | Method | Path | Description | Auth | | ------ | ---------------------------------------- | ---------------------------------- | -------- | | GET | `/api/marketplace/bases` | List project templates | Optional | | GET | `/api/marketplace/bases/browse` | Browse with pagination and filters | Optional | | GET | `/api/marketplace/bases/{slug}` | Get base details | Optional | | GET | `/api/marketplace/bases/{slug}/versions` | List available git tag versions | Optional | | POST | `/api/marketplace/bases` | Submit a new base | Yes | | GET | `/api/marketplace/bases/{id}/reviews` | Get base reviews | Optional | | POST | `/api/marketplace/bases/{id}/reviews` | Submit a review | Yes | ### User Library | Method | Path | Description | Auth | | ------ | --------------------------------- | --------------------- | ---- | | GET | `/api/marketplace/library/agents` | List purchased agents | Yes | | GET | `/api/marketplace/library/bases` | List purchased bases | Yes | ## Billing Endpoints Subscription management, credits, and usage tracking. | Method | Path | Description | Auth | | ------ | ------------------------------- | ------------------------------------- | ---- | | GET | `/api/billing/subscription` | Get current subscription details | Yes | | POST | `/api/billing/checkout` | Create Stripe checkout session | Yes | | POST | `/api/billing/portal` | Create Stripe customer portal session | Yes | | GET | `/api/billing/credits` | Get credit balance | Yes | | POST | `/api/billing/credits/purchase` | Purchase credit package | Yes | | GET | `/api/billing/usage` | Get usage history | Yes | | POST | `/api/webhooks/stripe` | Stripe webhook handler | No | ## Admin Endpoints Platform administration (requires superuser role). | Method | Path | Description | Auth | | ------ | ------------------------ | ------------------------------ | --------- | | GET | `/api/admin/metrics` | Platform-wide metrics | Superuser | | GET | `/api/admin/users` | List all users | Superuser | | GET | `/api/admin/users/{id}` | Get user details | Superuser | | PATCH | `/api/admin/users/{id}` | Update user (ban, change tier) | Superuser | | GET | `/api/admin/projects` | List all projects | Superuser | | GET | `/api/admin/agents` | List all marketplace agents | Superuser | | PATCH | `/api/admin/agents/{id}` | Moderate an agent | Superuser | ## Theme Endpoints Public theme API (no authentication required). | Method | Path | Description | Auth | | ------ | ------------------ | ------------------------- | ---- | | GET | `/api/themes` | List all available themes | No | | GET | `/api/themes/{id}` | Get theme details | No | ## Additional Endpoints ### Shell Sessions | Method | Path | Description | Auth | | ------ | ---------------------------- | -------------------------- | ---- | | POST | `/api/shell/sessions` | Create a shell session | Yes | | WS | `/api/shell/ws/{session_id}` | WebSocket for terminal I/O | Yes | | DELETE | `/api/shell/sessions/{id}` | Close a shell session | Yes | ### Secrets (Environment Variables) | Method | Path | Description | Auth | | ------ | --------------------------------- | -------------------- | ---- | | GET | `/api/secrets/{project_id}` | List project secrets | Yes | | POST | `/api/secrets/{project_id}` | Set a secret | Yes | | DELETE | `/api/secrets/{project_id}/{key}` | Delete a secret | Yes | ### Kanban (Task Board) | Method | Path | Description | Auth | | ------ | ---------------------------------------------- | ---------------- | ---- | | GET | `/api/kanban/{project_id}` | Get kanban board | Yes | | POST | `/api/kanban/{project_id}/columns` | Create a column | Yes | | PATCH | `/api/kanban/{project_id}/columns/{id}` | Update a column | Yes | | DELETE | `/api/kanban/{project_id}/columns/{id}` | Delete a column | Yes | | POST | `/api/kanban/{project_id}/tasks` | Create a task | Yes | | PATCH | `/api/kanban/{project_id}/tasks/{id}` | Update a task | Yes | | DELETE | `/api/kanban/{project_id}/tasks/{id}` | Delete a task | Yes | | POST | `/api/kanban/{project_id}/tasks/{id}/comments` | Add a comment | Yes | ### Background Tasks | Method | Path | Description | Auth | | ------ | ---------------------- | -------------------------- | ---- | | GET | `/api/tasks/{task_id}` | Get background task status | Yes | ### Feedback | Method | Path | Description | Auth | | ------ | ----------------------------- | ------------------- | ---- | | GET | `/api/feedback` | List feedback posts | Yes | | POST | `/api/feedback` | Submit feedback | Yes | | POST | `/api/feedback/{id}/upvote` | Upvote a post | Yes | | POST | `/api/feedback/{id}/comments` | Comment on a post | Yes | ### Referrals and Creators | Method | Path | Description | Auth | | ------ | ------------------------- | ------------------------- | ---- | | GET | `/api/referrals/code` | Get user's referral code | Yes | | POST | `/api/referrals/apply` | Apply a referral code | Yes | | GET | `/api/creators/dashboard` | Creator program dashboard | Yes | | POST | `/api/creators/payout` | Request creator payout | Yes | ### Health and Configuration | Method | Path | Description | Auth | | ------ | ------------- | ------------------------------------------- | ---- | | GET | `/health` | Backend health check | No | | GET | `/api/config` | Public config (deployment mode, app domain) | No | ## Common Response Patterns ```json theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "My App", "slug": "my-app-k3x8n2", "created_at": "2026-01-09T12:00:00Z", "status": "running" } ``` ```json theme={null} { "detail": "Project not found" } ``` Common status codes: * `400` Bad Request (invalid input, validation errors) * `401` Unauthorized (authentication required) * `403` Forbidden (insufficient permissions) * `404` Not Found (resource does not exist) * `422` Unprocessable Entity (Pydantic validation error) * `500` Internal Server Error (unexpected server error) ```json theme={null} { "items": [...], "total": 42, "page": 1, "total_pages": 3 } ``` List endpoints support pagination via `skip` and `limit` query parameters (or `page` and `limit` for marketplace browse endpoints). # Container Orchestration Source: https://docs.tesslate.com/developer/container-orchestration How Docker and Kubernetes manage user project containers ## Overview Every user project in OpenSail runs in an isolated containerized environment. Projects can have multiple containers (frontend, backend, database) that communicate over a private network. The orchestration layer manages container lifecycle, file operations, shell execution, resource management, and cleanup. This page covers both deployment modes (Docker and Kubernetes), the factory pattern that abstracts them, the S3 Sandwich pattern for hibernation, the EBS VolumeSnapshot lifecycle, pod affinity for multi-container projects, and the cleanup CronJob. ## The Factory Pattern All code accesses the orchestrator through a factory function. The factory reads `config.DEPLOYMENT_MODE` and returns the appropriate implementation. ```python theme={null} from orchestrator.app.services.orchestration import get_orchestrator orchestrator = get_orchestrator() # Returns DockerOrchestrator or KubernetesOrchestrator await orchestrator.start_project(project, containers, connections, user_id, db) ``` Never instantiate orchestrators directly. Always use `get_orchestrator()` to ensure singleton caching and correct mode selection. ### BaseOrchestrator Interface Both implementations conform to the same abstract interface: ```python theme={null} class BaseOrchestrator(ABC): @abstractmethod async def start_project(self, project, containers, connections, user_id, db): ... @abstractmethod async def stop_project(self, project_slug, project_id, user_id): ... @abstractmethod async def read_file(self, user_id, project_id, container_name, file_path, ...): ... @abstractmethod async def write_file(self, user_id, project_id, container_name, file_path, content, ...): ... @abstractmethod async def execute_command(self, user_id, project_id, container_name, command, ...): ... @abstractmethod async def cleanup_idle_environments(self, idle_timeout_minutes): ... ``` This ensures feature parity between modes. API endpoints use this interface without caring about the underlying infrastructure. ## Docker Mode (Local Development) **Source file:** `orchestrator/app/services/orchestration/docker.py` Docker mode uses Docker Compose to manage user projects. Each project gets: * A dedicated Docker network for isolation * Services defined in a dynamically generated `docker-compose.yml` * Traefik routing for `*.localhost` URLs * Direct filesystem access via shared volume (`/projects`) **Key characteristics:** * Fast iteration with no cluster overhead * Simple file operations (orchestrator reads the filesystem directly) * Traefik auto-discovery via container labels * Two-tier cleanup: scale to 0, then delete after longer timeout **URL pattern:** `http://{project-slug}-{container-name}.localhost` **Configuration:** ```bash theme={null} DEPLOYMENT_MODE=docker APP_DOMAIN=localhost USE_DOCKER_VOLUMES=true ``` ### Docker Mode: How It Works The orchestrator generates a Compose file from the project's Container models. Each container becomes a service with the correct image, ports, volumes, and Traefik labels. The generated file is executed with `docker-compose up -d`. Containers start in the background. Traefik discovers containers via labels and routes `*.localhost` URLs to the correct container ports. The orchestrator reads and writes files directly on the host filesystem at `users/{user_id}/{project_slug}/`. No pod exec required. Idle projects are first scaled to zero replicas, then deleted after a longer timeout. ## Kubernetes Mode (Production) **Source file:** `orchestrator/app/services/orchestration/kubernetes_orchestrator.py` Kubernetes mode creates a namespace per project with: * PersistentVolumeClaim (PVC) for file storage * File-manager pod (always running for file operations) * Dev container pods (started on-demand) * NetworkPolicy for project isolation * NGINX Ingress for HTTPS routing **Key characteristics:** * Scalable to thousands of concurrent projects * EBS VolumeSnapshot pattern for near-instant hibernation and restore * Pod affinity for shared RWO storage in multi-container projects * S3 credentials never exposed to user pods **URL pattern:** `https://{project-slug}-{container-directory}.{domain}` **Configuration:** ```bash theme={null} DEPLOYMENT_MODE=kubernetes K8S_DEVSERVER_IMAGE=tesslate-devserver:latest K8S_STORAGE_CLASS=tesslate-block-storage K8S_PVC_SIZE=10Gi K8S_PVC_ACCESS_MODE=ReadWriteOnce K8S_SNAPSHOT_CLASS=tesslate-ebs-snapshots K8S_ENABLE_POD_AFFINITY=true K8S_ENABLE_NETWORK_POLICIES=true K8S_WILDCARD_TLS_SECRET=tesslate-wildcard-tls K8S_HIBERNATION_IDLE_MINUTES=30 ``` ### Three Separate Lifecycles A critical design principle in Kubernetes mode is the separation of three distinct lifecycles: 1. User opens project; namespace + PVC + file-manager pod created. 2. User adds container to graph; file-manager runs `git clone` to `/app/{subdir}/`. 3. Files persist on PVC. 1. User clicks "Start"; Deployment + Service + Ingress created. 2. Dev container mounts existing PVC (files already present; no init containers). 3. User clicks "Stop"; Deployment deleted, files remain on PVC. 1. User leaves or idle timeout; VolumeSnapshot created from PVC (under 5 seconds). 2. Namespace deleted (including PVC). 3. User returns; PVC created from snapshot, EBS lazy-loads data (near-instant restore). ### File-Manager Pod The file-manager pod is always running while a project is open. | Property | Value | | --------- | -------------------------------- | | Image | `tesslate-devserver:latest` | | Command | `tail -f /dev/null` (keep alive) | | Volume | PVC mounted at `/app` | | Resources | 256Mi to 1536Mi RAM | **Responsibilities:** * Handle file read/write when dev containers are not running * Execute `git clone` when containers are added to the architecture graph * Keep PVC mounted to prevent unbound state * Provide consistent file access regardless of dev container state ### Dev Container Pods Dev containers are created on-demand when the user clicks "Start." The orchestrator reads `TESSLATE.md` from the file-manager pod to determine port, startup command, and framework. Image: `tesslate-devserver:latest`. Volume: existing PVC at `/app`. Working directory: `/app/{container_directory}`. Command: startup command runs in a tmux session. ClusterIP service with selector by container-id. NGINX Ingress with TLS using the wildcard certificate. `https://{slug}-{container}.saipriya.org` ## EBS VolumeSnapshot Lifecycle The EBS VolumeSnapshot pattern hibernates idle projects to save compute resources while preserving the full filesystem state (including `node_modules`). ``` +-----------------------------------------------------------+ | ACTIVE STATE | | Namespace: proj-{uuid} | | +-- PVC (10Gi EBS) | | +-- File-manager pod | | +-- Dev container pods (if started) | +-----------------------------------------------------------+ | | Idle 30+ minutes v +-----------------------------+ | HIBERNATION (< 5 seconds) | | | | 1. Create VolumeSnapshot | | 2. Wait for readyToUse | | 3. Delete namespace | +-----------------------------+ | v +-----------------------------------------------------------+ | HIBERNATED STATE | | VolumeSnapshot: snap-{project_id}-{timestamp} | | Database: environment_status = 'hibernated' | | Database: ProjectSnapshot record (status = 'ready') | +-----------------------------------------------------------+ | | User returns v +-----------------------------+ | RESTORATION (< 10 seconds) | | | | 1. Create PVC from snapshot | | 2. Create namespace + pods | | 3. EBS lazy-loads on access | +-----------------------------+ | v +-----------------------------------------------------------+ | ACTIVE STATE | | node_modules preserved; no npm install needed | +-----------------------------------------------------------+ ``` ### Snapshot Operations | Operation | What Happens | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Create** | VolumeSnapshot created from PVC. Non-blocking; poll until `readyToUse: true`. ProjectSnapshot database record created. | | **Restore** | PVC created with `dataSource` pointing to VolumeSnapshot. EBS provisioner creates new volume from snapshot with lazy-loading. | | **Soft-delete** | When a project is permanently deleted, snapshots are marked for 30-day retention. | | **Permanent delete** | Daily cleanup CronJob deletes expired snapshots after 30 days. | ### Timeline UI Users can create up to 5 manual snapshots per project for version history, accessible through the Timeline UI. Each snapshot preserves the entire filesystem state and can be restored at any time. ## S3 Sandwich Pattern (Legacy / Fallback) For environments without VolumeSnapshot support (e.g., Minikube), the S3 Sandwich pattern provides an alternative: Init process downloads the project archive from S3 to the PVC. Fast local I/O resumes. All file operations happen on the local PVC with disk-speed performance. PreStop hook archives the project from PVC back to S3 before pod termination. **Benefits:** Fast I/O during runtime, durability via S3 backup, cost efficiency with ephemeral PVCs. **Trade-offs:** Hydration time on first access, dehydration time on shutdown, S3 storage costs. ## Pod Affinity for Multi-Container Projects When a project has multiple containers (e.g., frontend + backend), all pods must run on the same Kubernetes node because PVCs use `ReadWriteOnce` (RWO) access mode. ```python theme={null} affinity = client.V1Affinity( pod_affinity=client.V1PodAffinity( required_during_scheduling_ignored_during_execution=[ client.V1PodAffinityTerm( label_selector=client.V1LabelSelector( match_labels={"tesslate.io/project-id": str(project_id)} ), topology_key="kubernetes.io/hostname" ) ] ) ) ``` This ensures all pods with the same `project-id` label land on the same node, enabling shared RWO volume access and improving inter-container communication performance. ## NetworkPolicy Isolation Each project namespace receives a NetworkPolicy that enforces: * **Allowed ingress:** From NGINX Ingress controller, from Tesslate backend (file operations), within the namespace (inter-container) * **Allowed egress:** DNS (UDP 53), HTTPS (TCP 443), HTTP (TCP 80) * **Denied:** All cross-project communication, access to internal cluster services not explicitly allowed ## Cleanup CronJob Two Kubernetes CronJobs manage automatic cleanup: **Schedule:** Every 2 minutes (`*/2 * * * *`) Queries the database for projects where `environment_status = 'active'` and `last_activity` is older than the idle timeout (default: 30 minutes). For each idle project: 1. Creates a VolumeSnapshot 2. Deletes the namespace 3. Updates the database status to "hibernated" 4. Sends a WebSocket notification to redirect the user **Schedule:** Daily at 3 AM UTC (`0 3 * * *`) Queries for soft-deleted ProjectSnapshot records older than 30 days and permanently deletes the associated VolumeSnapshots from the cluster. ## Debugging Commands ```bash theme={null} # View generated compose file cat docker-compose-projects/my-project-abc123.yml # Check container logs docker logs my-project-abc123-frontend # List networks docker network ls | grep tesslate # List project files ls /projects/my-project-abc123/ ``` ```bash theme={null} PROJECT_ID= NAMESPACE=proj-$PROJECT_ID # Check all resources in project namespace kubectl get all -n $NAMESPACE # File-manager logs kubectl logs -n $NAMESPACE deployment/file-manager -c file-manager # Dev container logs kubectl logs -n $NAMESPACE deployment/dev-frontend -c dev-server # Exec into file-manager kubectl exec -n $NAMESPACE deployment/file-manager -c file-manager -- ls -la /app # Check ingress kubectl get ingress -n $NAMESPACE # Check PVC status kubectl get pvc -n $NAMESPACE # Check NetworkPolicy kubectl describe networkpolicy project-isolation -n $NAMESPACE # Check VolumeSnapshot status kubectl get volumesnapshot -n $NAMESPACE ``` ## Key Source Files | File | Purpose | | -------------------------------------------------------------------- | ---------------------------------- | | `orchestrator/app/services/orchestration/base.py` | Abstract orchestrator interface | | `orchestrator/app/services/orchestration/factory.py` | Factory pattern and mode detection | | `orchestrator/app/services/orchestration/docker.py` | Docker Compose orchestrator | | `orchestrator/app/services/orchestration/kubernetes_orchestrator.py` | Kubernetes orchestrator | | `orchestrator/app/services/orchestration/kubernetes/client.py` | K8s API wrapper | | `orchestrator/app/services/orchestration/kubernetes/helpers.py` | Manifest generation functions | | `orchestrator/app/services/snapshot_manager.py` | EBS VolumeSnapshot operations | | `orchestrator/app/services/s3_manager.py` | S3 hydration/dehydration | | `orchestrator/app/services/activity_tracker.py` | Database-based activity tracking | # Contributing Guide Source: https://docs.tesslate.com/developer/contributing How to set up a development environment and contribute to OpenSail ## Overview This guide walks you through setting up a local development environment, understanding the codebase structure, and contributing changes to OpenSail. Whether you want to add a new API endpoint, extend the AI agent with a new tool, or fix a bug, this page covers the full workflow. ## Prerequisites | Software | Version | Purpose | | -------------- | ------- | -------------------------------------------------------- | | Docker Desktop | Latest | Container runtime (PostgreSQL + user project containers) | | Node.js | 20+ | Frontend development | | Python | 3.11+ | Backend development | | Git | Latest | Version control | **System requirements:** * 8 GB RAM minimum (16 GB recommended) * 20 GB free disk space * Docker Desktop running with WSL 2 (Windows) or native (macOS/Linux) ## Local Development Setup You have two options: Docker Compose (recommended for getting started quickly) or native development (recommended for debugging with breakpoints). ```bash theme={null} git clone https://github.com/your-org/tesslate-studio.git cd tesslate-studio ``` ```bash theme={null} cp .env.example .env ``` Edit `.env` and set the required values: ```bash theme={null} SECRET_KEY=your-secret-key-here-change-this DEPLOYMENT_MODE=docker POSTGRES_DB=tesslate_dev POSTGRES_USER=tesslate_user POSTGRES_PASSWORD=dev_password_change_me LITELLM_API_BASE=https://your-litellm-url.com/v1 LITELLM_MASTER_KEY=your-litellm-master-key APP_PROTOCOL=http APP_DOMAIN=localhost ``` ```bash theme={null} docker compose up --build -d docker compose ps # Verify all services are healthy ``` This image is required for user project containers: ```bash theme={null} docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/ ``` ```bash theme={null} docker exec tesslate-orchestrator alembic upgrade head # Copy and run seed scripts docker cp scripts/seed/seed_marketplace_bases.py tesslate-orchestrator:/tmp/ docker cp scripts/seed/seed_marketplace_agents.py tesslate-orchestrator:/tmp/ docker cp scripts/seed/seed_opensource_agents.py tesslate-orchestrator:/tmp/ docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed_marketplace_bases.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed_marketplace_agents.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed_opensource_agents.py ``` | Service | URL | | ----------- | -------------------------------------------------------- | | Frontend | [http://localhost](http://localhost) | | Backend API | [http://localhost:8000](http://localhost:8000) | | Swagger UI | [http://localhost:8000/docs](http://localhost:8000/docs) | ```bash theme={null} git clone https://github.com/your-org/tesslate-studio.git cd tesslate-studio ``` You still need PostgreSQL in Docker: ```bash theme={null} docker compose up -d postgres docker compose ps postgres # Verify it's healthy ``` ```bash theme={null} cd orchestrator python -m venv .venv # Activate virtual environment # macOS/Linux: source .venv/bin/activate # Windows PowerShell: .\.venv\Scripts\Activate.ps1 pip install -e ".[dev]" ``` ```bash theme={null} cd app npm install ``` Copy `.env.example` to `.env` (same as Docker tab). For the frontend, create `app/.env`: ```bash theme={null} VITE_API_URL=http://localhost:8000 ``` ```bash theme={null} cd orchestrator alembic upgrade head ``` **Terminal 1 (Backend):** ```bash theme={null} cd orchestrator source .venv/bin/activate uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 ``` **Terminal 2 (Frontend):** ```bash theme={null} cd app npm run dev ``` | Service | URL | | ----------- | -------------------------------------------------------- | | Frontend | [http://localhost:5173](http://localhost:5173) | | Backend API | [http://localhost:8000](http://localhost:8000) | | Swagger UI | [http://localhost:8000/docs](http://localhost:8000/docs) | | PostgreSQL | localhost:5432 | ## Project Structure ``` tesslate-studio/ +-- orchestrator/ # FastAPI backend | +-- app/ | | +-- main.py # Application entry point | | +-- config.py # Settings and configuration | | +-- models.py # SQLAlchemy models | | +-- schemas.py # Pydantic schemas | | +-- routers/ # API endpoints (25+ files) | | +-- services/ # Business logic (30+ files) | | +-- agent/ # AI agent system | | | +-- tools/ # Agent tools | | +-- middleware/ # CSRF, auth middleware | +-- alembic/ # Database migrations | +-- tests/ # Backend tests | +-- Dockerfile # Backend container | +-- Dockerfile.devserver # User project container image | +-- app/ # React frontend | +-- src/ | | +-- pages/ # Page components | | +-- components/ # Reusable components | | +-- lib/ # Utilities and API client | | +-- contexts/ # React contexts (Auth, Command) | +-- public/ # Static assets | +-- Dockerfile.prod # Frontend container | +-- k8s/ # Kubernetes manifests (Kustomize) +-- scripts/ # Seed scripts, deployment helpers +-- docker-compose.yml # Local development setup ``` ## Adding a New API Router Create a new file in `orchestrator/app/routers/`. Follow the naming convention of existing routers. ```python theme={null} from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from ..database import get_db from ..users import current_active_user from ..models_auth import User router = APIRouter(prefix="/api/your-feature", tags=["your-feature"]) @router.get("/") async def list_items( current_user: User = Depends(current_active_user), db: AsyncSession = Depends(get_db) ): """List all items for the current user.""" # Implementation here pass ``` Add schemas in `orchestrator/app/schemas.py` (or a new `schemas_*.py` file): ```python theme={null} from pydantic import BaseModel, ConfigDict from uuid import UUID from datetime import datetime class YourFeatureCreate(BaseModel): name: str class YourFeatureResponse(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID name: str created_at: datetime ``` Open `orchestrator/app/main.py` and add: ```python theme={null} from .routers import your_feature app.include_router(your_feature.router) ``` Add models to `orchestrator/app/models.py`, then generate a migration. Create tests in `orchestrator/tests/routers/test_your_feature.py`: ```python theme={null} import pytest from httpx import AsyncClient @pytest.mark.asyncio async def test_list_items(client: AsyncClient, auth_headers: dict): response = await client.get("/api/your-feature", headers=auth_headers) assert response.status_code == 200 ``` ## Adding Agent Tools Create a new directory under `orchestrator/app/agent/tools/`: ``` orchestrator/app/agent/tools/ +-- your_tool/ +-- __init__.py +-- implementation.py ``` The executor function receives `params` (tool parameters) and `context` (execution context with user\_id, project\_id, etc.): ```python theme={null} from ..registry import Tool, ToolCategory from ..output_formatter import success_output, error_output async def your_tool_executor(params, context): file_path = params.get("file_path") if not file_path: return error_output( message="file_path is required", suggestion="Provide the path to the file" ) # Use orchestrator for file/shell operations from ....services.orchestration import get_orchestrator orchestrator = get_orchestrator() try: result = await orchestrator.read_file( user_id=context["user_id"], project_id=str(context["project_id"]), container_name=context.get("container_name"), file_path=file_path, project_slug=context.get("project_slug"), subdir=context.get("container_directory") ) return success_output(message="Success", result=result) except Exception as e: return error_output(message=f"Failed: {str(e)}") ``` Add a registration function and wire it into the registry: ```python theme={null} def register_your_tools(registry): registry.register(Tool( name="your_tool", description="What this tool does (shown to the LLM)", parameters={ "type": "object", "properties": { "file_path": { "type": "string", "description": "Path relative to project root" } }, "required": ["file_path"] }, executor=your_tool_executor, category=ToolCategory.FILE_OPS, examples=['{"tool_name": "your_tool", "parameters": {"file_path": "src/App.tsx"}}'] )) ``` Then edit `orchestrator/app/agent/tools/registry.py` to import and call your registration function in `_register_all_tools()`. Write unit tests with mocked dependencies: ```python theme={null} @pytest.mark.asyncio async def test_your_tool_success(): params = {"file_path": "src/App.jsx"} context = {"user_id": "test", "project_id": "test", "project_slug": "test"} with patch('...get_orchestrator') as mock: mock.return_value.read_file = AsyncMock(return_value="content") result = await your_tool_executor(params, context) assert result["success"] is True ``` ## Database Migrations with Alembic Edit `orchestrator/app/models.py` (or `models_auth.py` / `models_kanban.py`). ```bash theme={null} cd orchestrator alembic revision --autogenerate -m "description_of_changes" ``` Check the new file in `orchestrator/alembic/versions/`. Verify: * Correct column types and nullable settings * Proper index and constraint names * No unintended data loss ```bash theme={null} alembic upgrade head ``` ```bash theme={null} alembic downgrade -1 # Rollback alembic upgrade head # Re-apply ``` Alembic autogenerate cannot detect column renames (it sees delete + add). For renames, create a manual migration with `alembic revision -m "rename_column"` and use `op.alter_column()`. ### Running Migrations in Production ```bash theme={null} # Docker Compose docker exec tesslate-orchestrator alembic upgrade head # Kubernetes kubectl exec -n tesslate deployment/tesslate-backend -- alembic upgrade head ``` ## Code Style and Patterns ### Backend (Python) * **Async everywhere:** All I/O operations must use `async/await`. * **Dependency injection:** Receive database sessions and config as function parameters; never create sessions inside services. * **Factory pattern:** Use `get_orchestrator()` for container operations. * **Error handling:** Use `HTTPException` with appropriate status codes; log errors with context. * **Non-blocking:** Use `BackgroundTasks` for long-running operations. ```python theme={null} # Good: Non-blocking, returns immediately @router.post("/start") async def start_project(background_tasks: BackgroundTasks): background_tasks.add_task(setup_containers, project) return {"status": "starting"} # Bad: Blocks for 30+ seconds @router.post("/start") async def start_project(): await setup_containers(project) return {"status": "started"} ``` ### Frontend (TypeScript) * **Functional components:** Use React hooks, not class components. * **Type safety:** Define interfaces for all props and API responses. * **API calls:** Use the centralized `api.ts` client. * **State management:** Use React Context for global state; local state for component-specific data. ## Running Tests ```bash theme={null} # Backend tests cd orchestrator pytest # Frontend tests cd app npm test ``` ## Common Development Tasks ```bash theme={null} docker compose down docker volume rm tesslate-postgres-dev-data docker compose up -d postgres cd orchestrator && alembic upgrade head ``` ```bash theme={null} docker compose exec postgres psql -U tesslate_user -d tesslate_dev ``` ```bash theme={null} docker rmi -f tesslate-backend:latest docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/ ``` ```bash theme={null} # Docker Compose docker compose logs -f orchestrator # Kubernetes kubectl logs -f deployment/tesslate-backend -n tesslate ``` ## Pull Request Process ```bash theme={null} git checkout -b feature/your-feature-name ``` Follow the code style and patterns described above. Write tests for new functionality. Ensure all existing and new tests pass. Keep commits focused on a single logical change. Include a description of what changed and why. Reference any related issues. Update the PR based on reviewer comments. Keep the conversation focused on code quality and correctness. # Data Flow Patterns Source: https://docs.tesslate.com/developer/data-flow How requests flow through OpenSail from frontend to backend ## Overview Every user interaction in OpenSail follows a consistent request/response pattern that flows through the frontend, orchestrator, database, and (optionally) the container runtime. This page documents the lifecycle of each major flow: general API requests, agent chat, file operations, container management, Git operations, deployments, and streaming patterns. If you are new to the codebase, start with the General API Request Flow to understand the common pattern, then explore the specific flows relevant to your work. ## General API Request Flow All user interactions follow this eight-step lifecycle. The user performs an action in the browser (click, type, navigate). The React app sends an HTTP or WebSocket request to the Orchestrator. Authentication is included via `Authorization: Bearer {jwt}` header or session cookie. FastAPI middleware decodes the JWT token, verifies the user session, and checks permissions (RBAC). The Orchestrator queries or updates PostgreSQL using async SQLAlchemy. Depending on the request type, the Orchestrator delegates to the appropriate subsystem: * **File operation**: Container filesystem (direct in Docker, pod exec in K8s) * **Container operation**: Docker Compose or Kubernetes API * **AI chat**: LiteLLM proxy to OpenAI/Anthropic * **Deployment**: Vercel/Netlify/Cloudflare API The Orchestrator assembles the JSON response from the operation result and database state. The response is sent back to the React app over the same HTTP connection (or as SSE/WebSocket events for streaming). The frontend updates its state and re-renders the relevant components. ### Request Flow Diagram ``` +----------+ | User | | Browser | +----+-----+ | | 1. User interaction (click, type, etc.) v +----------------+ | Frontend | | (React App) | +----+-----------+ | | 2. HTTP/WebSocket request | Authorization: Bearer {jwt} OR Cookie: {session} v +----------------+ | Orchestrator | | (FastAPI API) | +----+-----------+ | | 3. Validate authentication | 4. Query/update database v +----------------+ | PostgreSQL | | Database | +----+-----------+ | | 5. Database response v +----------------+ | Orchestrator | 6. Perform operation: | (FastAPI API) | File op -> Container filesystem +----+-----------+ Container op -> Docker/K8s API | AI chat -> LiteLLM -> AI provider | 7. Return JSON Deployment -> Vercel/Netlify API v +----------------+ | Frontend | | (React App) | 8. Update UI with response data +----------------+ ``` ## Agent Chat Flow The agent chat is the most complex data flow, involving LLM calls, tool execution, and real-time streaming to the frontend via Server-Sent Events (SSE). The user enters a message in the chat UI (e.g., "Create a React component for a todo list"). The frontend sends `POST /api/chat/stream` with `{ project_id, message, chat_id }` and opens an EventSource for streaming. The chat router loads previous messages from the database and builds conversation context. `agent/factory.py` instantiates a `tesslate-agent` with the appropriate system prompt, available tools (read\_file, write\_file, bash\_exec, etc.), and LLM model. The `tesslate-agent` enters a loop: 1. Call the LLM with system prompt + conversation history 2. If the LLM returns tool calls, execute them (e.g., `write_file`, `bash_exec`) 3. Stream each tool execution event to the frontend 4. Call the LLM again with tool results 5. Repeat until the LLM produces a final text response The agent streams its final message to the frontend, which renders it in real-time in the chat UI. ### Agent Tool Execution Example **User prompt:** "Create a React component for a todo list" ``` LLM Call 1: Input: System prompt + user message Output: Tool call: write_file("src/TodoList.tsx", "import React...") Tool Execution (write_file): Docker mode: Write to users/{user_id}/{project_slug}/src/TodoList.tsx K8s mode: Exec into file-manager pod, write to /app/src/TodoList.tsx Stream Event to Frontend: { "type": "tool_execution", "tool": "write_file", "args": { "path": "src/TodoList.tsx" }, "result": "File created successfully" } LLM Call 2: Input: Previous context + tool result Output: "I've created a TodoList component in src/TodoList.tsx..." Stream Event to Frontend: { "type": "message", "content": "I've created a TodoList component..." } ``` ### Available Agent Tools | Tool | File | Purpose | | --------------------------- | ------------------------------------- | ----------------------------------- | | `read_file` / `write_file` | `agent/tools/file_ops/read_write.py` | Read and write files in the project | | `patch_file` / `multi_edit` | `agent/tools/file_ops/edit.py` | Edit specific file sections | | `bash_exec` | `agent/tools/shell_ops/bash.py` | Execute shell commands | | `shell_exec` / `shell_open` | `agent/tools/shell_ops/session.py` | Persistent shell sessions | | `web_fetch` | `agent/tools/web_ops/fetch.py` | HTTP requests for web content | | `todos` | `agent/tools/planning_ops/todos.py` | Task planning and tracking | | `get_project_info` | `agent/tools/project_ops/metadata.py` | Query project information | ## File Operations Flow File reads and writes differ depending on deployment mode. In Docker mode, the orchestrator accesses the filesystem directly. In Kubernetes mode, it executes commands inside the file-manager pod. ``` User clicks file in browser | v Frontend: GET /api/projects/{id}/files/{path} | v Orchestrator validates auth, gets project | v Check deployment mode: Docker mode: file_path = "users/{user_id}/{slug}/{path}" content = open(file_path).read() Kubernetes mode: namespace = "proj-{project_id}" pod = file-manager pod content = kubectl exec cat /app/{subdir}/{path} | v Return { content } to frontend | v Display in Monaco editor ``` ``` User edits file in Monaco editor | v Frontend: PUT /api/projects/{id}/files/{path} Body: { content: "..." } | v Orchestrator validates auth, gets project | v Check deployment mode: Docker mode: file_path = "users/{user_id}/{slug}/{path}" open(file_path, 'w').write(content) Kubernetes mode: Encode content as base64 kubectl exec mkdir -p {dir_path} kubectl exec sh -c "echo '{base64}' | base64 -d > {path}" | v Update project.last_activity | v Return { status: "success" } to frontend ``` ## Container Operations Flow Container start and stop operations are non-blocking. The Orchestrator returns immediately and the frontend polls for status updates. ### Start Project Containers Frontend sends `POST /api/projects/{id}/start`. The Orchestrator validates auth, checks that the project is not already running, queues a background task for container setup, and returns `{ "status": "starting" }` immediately. The frontend polls `GET /api/projects/{id}/status` every 2 seconds. 1. Create namespace (`proj-{uuid}`) 2. Create PVC (shared storage, e.g. 10Gi RWO) 3. Restore from VolumeSnapshot if hibernated (or hydrate from S3 for legacy projects) 4. Create file-manager pod (always running) 5. For each container: create Deployment + Service + Ingress 6. Create NetworkPolicy for isolation 7. Update project status in database to "running" 8. Return container URLs Status poll detects "running". The frontend displays container URLs and enables the live preview iframe. ### Stop Project Containers Frontend sends `POST /api/projects/{id}/stop`. 1. Create VolumeSnapshot from PVC (under 5 seconds) 2. Wait for snapshot readiness 3. Delete namespace (cascades to all resources: Deployments, Services, Ingress, PVC, NetworkPolicy) 4. Update project status to "hibernated" Status poll detects "stopped" or "hibernated". Live preview is disabled and the Start button appears. In Kubernetes mode, hibernation creates an EBS VolumeSnapshot that preserves the entire filesystem state including `node_modules`. No `npm install` is needed on restore. Projects restore in under 10 seconds thanks to EBS lazy-loading. ## Git Operations Flow ### Clone Repository ``` User clicks "Import from GitHub" | v Frontend: POST /api/git/clone Body: { repo_url, project_id } | v Orchestrator validates auth, queues background task Returns { "status": "cloning" } | v Background task: Kubernetes mode: Generate git clone script Execute in file-manager pod via kubectl exec Docker mode: Clone directly to filesystem: users/{user_id}/{slug}/ | v Frontend polls, detects "cloned" Refreshes file tree ``` ### Commit and Push ``` User (or agent) clicks "Commit" | v Frontend: POST /api/git/commit Body: { message, project_id } | v Orchestrator: Kubernetes mode: kubectl exec git config user.name {name} kubectl exec git config user.email {email} kubectl exec git add . kubectl exec git commit -m {message} kubectl exec git push Docker mode: Execute git commands on filesystem | v Return { "status": "pushed", "commit_hash": "..." } ``` ## Deployment Flow (External Providers) External deployments to Vercel, Netlify, or Cloudflare follow a consistent non-blocking pattern. Frontend sends `POST /api/deployments` with provider name, project ID, and configuration. The Orchestrator decrypts the user's stored `DeploymentCredential` for the chosen provider. 1. Build the project locally (e.g., `npm run build`) 2. Push to Git if needed (create/update GitHub repo) 3. Call provider API to create deployment 4. Poll provider API until deployment status is "READY" 5. Save deployment record to database A WebSocket message or status poll delivers the live URL to the frontend, which displays a success message with a link to the deployed application. ## WebSocket and SSE Streaming Patterns OpenSail uses two streaming mechanisms for real-time communication. **Backend (FastAPI):** ```python theme={null} async def stream_agent_response(project_id, message): async for event in agent.run(message): yield f"data: {json.dumps(event)}\n\n" @router.post("/stream") async def chat_stream(request: ChatRequest): return StreamingResponse( stream_agent_response(request.project_id, request.message), media_type="text/event-stream" ) ``` **Frontend (EventSource):** ```typescript theme={null} const eventSource = new EventSource('/api/chat/stream', { body: JSON.stringify({ message, project_id }), method: 'POST' }); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'tool_execution': displayToolExecution(data.tool, data.args); break; case 'message': displayAgentMessage(data.content); break; case 'error': displayError(data.error); break; } }; ``` **Use cases:** Agent chat streaming, build output streaming. **Frontend:** ```typescript theme={null} const pollStatus = async () => { const status = await api.get(`/api/projects/${id}/status`); if (status.state !== 'running') { setTimeout(pollStatus, 2000); // Poll every 2 seconds } }; ``` **Use cases:** Container startup status, deployment progress, background task status. **Backend (FastAPI):** ```python theme={null} @router.websocket("/ws/{chat_id}") async def websocket_endpoint(websocket: WebSocket, chat_id: str): await websocket.accept() try: while True: data = await websocket.receive_text() await websocket.send_text(response) except WebSocketDisconnect: pass ``` **Use cases:** Live terminal sessions, real-time log streaming, hibernation notifications. ## Performance Optimizations Long-running operations return immediately and execute in the background. The frontend polls for status. ```python theme={null} # Return immediately, execute in background @router.post("/") async def create_project(data: ProjectCreate, background_tasks: BackgroundTasks): project = await db_create_project(data) background_tasks.add_task(setup_containers, project) return project # Frontend polls /status ``` Use `selectinload()` to prevent N+1 queries and load related objects in a single query. ```python theme={null} # Single query with joined loading project = await db.execute( select(Project) .options( selectinload(Project.containers), selectinload(Project.containers) .selectinload(Container.connections) ) .where(Project.id == project_id) ) ``` | Pattern | When to Use | Example | | ------------------------ | ------------------------------------------ | ----------------------------- | | SSE (Server-Sent Events) | Unidirectional, real-time data from server | Agent chat responses | | Polling | Simple status checks, stateless | Container startup status | | WebSocket | Bidirectional, real-time communication | Live terminal, shell sessions | ## Key Source Files | File | Purpose | | -------------------------------------------------------------------- | -------------------------------------------------- | | `orchestrator/app/routers/projects.py` | Project CRUD, file operations, container lifecycle | | `orchestrator/app/routers/chat.py` | Agent chat and streaming | | `orchestrator/app/routers/git.py` | Git operations | | `orchestrator/app/routers/deployments.py` | External deployments | | `orchestrator/app/agent/stream_agent.py` | Streaming AI agent implementation | | `orchestrator/app/agent/factory.py` | Agent creation from config | | `orchestrator/app/agent/tools/` | Agent tool implementations | | `orchestrator/app/services/orchestration/kubernetes_orchestrator.py` | K8s container management | | `orchestrator/app/services/s3_manager.py` | S3 hydration/dehydration | # Database Models Source: https://docs.tesslate.com/developer/database-models All 39+ SQLAlchemy models powering OpenSail ## Overview OpenSail uses PostgreSQL with SQLAlchemy 2.x (async) as the ORM. The schema consists of 39+ models organized into functional domains. All models use UUID primary keys, timezone-aware timestamps, and cascade deletes for clean data removal. This reference covers every model, its key fields, and its relationships. If you are adding a new model or modifying the schema, read the [Contributing Guide](/developer/contributing) section on database migrations. ## Common Patterns Before diving into individual models, here are the patterns shared across the codebase. ```python theme={null} from sqlalchemy.dialects.postgresql import UUID import uuid id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) ``` ```python theme={null} created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) ``` ```python theme={null} project_id = Column(UUID(as_uuid=True), ForeignKey("projects.id", ondelete="CASCADE")) ``` ```python theme={null} settings = Column(JSON, nullable=True) # Flexible metadata environment_vars = Column(JSON, nullable=True) # Container env vars tags = Column(JSON) # ["react", "typescript"] ``` ## Model Hierarchy ``` User (owner) +-> Projects (multiple) | +-> Containers (frontend, backend, db) | | +-> ContainerConnections (networking) | +-> BrowserPreviews (live preview windows) | +-> ProjectFiles (code files) | +-> ProjectAssets (images, fonts) | +-> Chats (AI conversations) | | +-> Messages (user/assistant) | | +-> AgentCommandLog (audit trail) | +-> ShellSessions (persistent terminals) | +-> KanbanBoard (task management) | +-> Deployments (external hosting) | +-> ProjectNotes (rich text notes) +-> UserPurchasedAgents -> MarketplaceAgents +-> DeploymentCredentials +-> GitProviderCredentials +-> UserAPIKeys / UserCustomModels ``` ## Auth Models Defined in `orchestrator/app/models_auth.py`. ### User The central identity model. Extends FastAPI-Users' `SQLAlchemyBaseUserTable` with Tesslate-specific features. | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------ | | `id` | UUID | Primary key | | `email` | String | Unique, indexed | | `hashed_password` | String | bcrypt hashed | | `name` | String | Display name | | `username` | String | Unique login identifier | | `slug` | String | URL-safe identifier | | `subscription_tier` | String | free, basic, pro, ultra | | `stripe_customer_id` | String | Stripe customer reference | | `bundled_credits` | Integer | Monthly allowance (resets on billing date) | | `purchased_credits` | Integer | Never expire | | `signup_bonus_credits` | Integer | Expires after N days | | `daily_credits` | Integer | Free tier daily allowance | | `theme_preset` | String | Current theme ID (default: "default-dark") | | `chat_position` | String | Chat panel: "left", "center", "right" | | `disabled_models` | JSON | Model IDs hidden from chat selector | | `two_fa_enabled` | Boolean | Whether 2FA is active | | `avatar_url` | String | Profile picture URL | | `referral_code` | String | Unique referral code | **Key relationships:** `projects`, `chats`, `purchased_agents`, `purchased_bases`, `api_keys`, `custom_models`, `deployment_credentials`, `oauth_accounts`, `access_tokens` ### OAuthAccount Stores OAuth provider tokens (GitHub, Google). Managed by FastAPI-Users. ### AccessToken JWT access tokens for session management. Managed by FastAPI-Users. ### EmailVerificationCode Stores hashed 2FA codes and password reset tokens. | Field | Type | Description | | -------------- | -------- | ------------------------------------ | | `user_id` | UUID | FK to users (CASCADE) | | `code_hash` | String | argon2 hashed code (never plaintext) | | `purpose` | String | "2fa\_login" or "password\_reset" | | `attempts` | Integer | Failed verification attempts | | `max_attempts` | Integer | Default: 5 | | `expires_at` | DateTime | Code expiration time | | `used` | Boolean | Whether code has been consumed | ## Core Models Defined in `orchestrator/app/models.py`. ### Project Top-level organizational unit. Each project can contain multiple containers forming a monorepo architecture. | Field | Type | Description | | ----------------------- | -------- | ------------------------------------------------------- | | `id` | UUID | Primary key | | `name` | String | Display name | | `slug` | String | URL-safe identifier (auto-generated with random suffix) | | `owner_id` | UUID | FK to users | | `description` | String | Project description | | `has_git_repo` | Boolean | Whether Git is initialized | | `git_remote_url` | String | Remote repository URL | | `architecture_diagram` | String | Mermaid diagram source | | `settings` | JSON | Project settings (preview\_mode, etc.) | | `network_name` | String | Docker network: `tesslate-{slug}` | | `environment_status` | String | active, hibernated, starting, stopping | | `last_activity` | DateTime | Last user interaction | | `hibernated_at` | DateTime | When project was hibernated | | `s3_archive_size_bytes` | Integer | Size of S3 archive | **Key relationships:** `owner`, `containers`, `files`, `assets`, `browser_previews`, `chats`, `project_agents`, `kanban_board`, `notes`, `deployments`, `shell_sessions` **Environment status lifecycle:** ``` active -> stopping -> hibernated hibernated -> starting -> active ``` ### Container Individual services within a project (frontend, backend, database). | Field | Type | Description | | --------------------------- | ------- | ------------------------------------------------------------------ | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `base_id` | UUID | FK to marketplace\_bases (NULL for custom) | | `name` | String | Display name: "frontend", "api", "database" | | `directory` | String | Directory in monorepo: "packages/frontend" | | `container_name` | String | Docker/K8s container name | | `port` | Integer | Exposed port (host side) | | `internal_port` | Integer | Port the dev server listens on inside container | | `environment_vars` | JSON | Environment variables | | `container_type` | String | "base" (user app) or "service" (infra) | | `service_slug` | String | For services: "postgres", "redis", etc. | | `deployment_mode` | String | "container" or "external" | | `external_endpoint` | String | For external: "[https://xxx.supabase.co](https://xxx.supabase.co)" | | `status` | String | stopped, starting, running, failed, connected | | `position_x` / `position_y` | Float | React Flow canvas position | **Key relationships:** `project`, `base`, `connections_from`, `connections_to` The `effective_port` property returns `internal_port or port or 3000`. Always use this instead of writing ad-hoc fallback chains. ### ContainerConnection Edges in the React Flow graph defining how containers communicate. | Field | Type | Description | | --------------------- | ------ | ----------------------------------------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `source_container_id` | UUID | Source container (arrow tail) | | `target_container_id` | UUID | Target container (arrow head) | | `connector_type` | String | env\_injection, http\_api, database, cache, websocket, etc. | | `config` | JSON | Connector-specific settings | | `label` | String | Optional edge label | ### BrowserPreview Live preview windows displayed on the React Flow canvas. | Field | Type | Description | | --------------------------- | ------ | ------------------------------------ | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `connected_container_id` | UUID | Container being previewed (nullable) | | `position_x` / `position_y` | Float | Canvas position | | `current_path` | String | Current URL path (default: "/") | ### ProjectFile Database-backed file content storage for the Monaco editor. | Field | Type | Description | | ------------ | ------ | ---------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `file_path` | String | Relative path: "src/App.tsx" | | `content` | Text | File content | ### ProjectAsset Tracks uploaded binary assets (images, fonts, videos). | Field | Type | Description | | ------------------ | ------- | ----------------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `filename` | String | Original filename | | `directory` | String | Target directory: "/public/images" | | `file_type` | String | image, video, font, document, other | | `file_size` | Integer | Size in bytes | | `mime_type` | String | MIME type: "image/png" | | `width` / `height` | Integer | Image dimensions (images only) | ### ProjectSnapshot EBS VolumeSnapshot records for project versioning and timeline. | Field | Type | Description | | --------------- | ------ | ---------------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `snapshot_type` | String | "hibernation" or "manual" | | `status` | String | pending, ready, restoring, deleted | | `label` | String | User-provided label | ## Chat Models ### Chat Conversation threads with AI agents. | Field | Type | Description | | ------------ | -------- | --------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `project_id` | UUID | FK to projects | | `created_at` | DateTime | When the chat started | **Key relationships:** `messages` (one-to-many) ### Message Individual messages in a chat (user or assistant). | Field | Type | Description | | ------------------ | ------ | --------------------------------- | | `id` | UUID | Primary key | | `chat_id` | UUID | FK to chats | | `role` | String | "user" or "assistant" | | `content` | Text | Message content | | `message_metadata` | JSON | Agent execution steps, tool calls | ### AgentCommandLog Audit log for shell commands executed by AI agents. | Field | Type | Description | | ------------------- | ------- | -------------------------------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `project_id` | UUID | FK to projects | | `command` | String | The shell command | | `working_dir` | String | Working directory | | `success` | Boolean | Whether execution succeeded | | `exit_code` | Integer | Process exit code | | `stdout` / `stderr` | Text | Command output | | `risk_level` | String | Risk assessment ("safe", "moderate", "high") | | `duration_ms` | Integer | Execution duration | ### ShellSession Persistent terminal sessions for WebSocket connections. | Field | Type | Description | | ------------------------------ | ------- | ------------------------- | | `id` | UUID | Primary key | | `session_id` | String | Unique session identifier | | `project_id` | UUID | FK to projects | | `container_name` | String | Target container | | `status` | String | active, closed | | `bytes_read` / `bytes_written` | Integer | Resource tracking | ## Marketplace Models ### MarketplaceAgent AI agents available for purchase or free use. | Field | Type | Description | | ----------------- | ------- | -------------------------------- | | `id` | UUID | Primary key | | `name` | String | Agent display name | | `slug` | String | URL-safe identifier | | `description` | Text | Agent description | | `system_prompt` | Text | LLM system prompt | | `tools` | JSON | List of enabled tool names | | `tool_configs` | JSON | Per-tool configuration overrides | | `model` | String | LLM model to use | | `pricing_type` | String | free, monthly, api, one\_time | | `price` | Integer | Price in cents | | `is_forkable` | Boolean | Can users fork this agent | | `parent_agent_id` | UUID | FK to self (for forks) | | `downloads` | Integer | Total download count | | `rating` | Float | Average rating (1-5) | ### MarketplaceBase Project templates (React, FastAPI, Next.js, etc.). | Field | Type | Description | | -------------------- | ------ | --------------------------------------------- | | `id` | UUID | Primary key | | `name` | String | Template name | | `slug` | String | URL-safe identifier | | `git_repo_url` | String | Source Git repository | | `category` | String | Template category | | `tech_stack` | JSON | Technologies used | | `pricing_type` | String | free or paid | | `created_by_user_id` | UUID | NULL for seeded, user UUID for user-submitted | | `visibility` | String | "private" or "public" | ### WorkflowTemplate Pre-configured multi-container workflows. | Field | Type | Description | | ---------------------- | ------ | ------------------------------------ | | `id` | UUID | Primary key | | `name` | String | Workflow name | | `template_definition` | JSON | Container and connection definitions | | `required_credentials` | JSON | Required deployment credentials | ### UserPurchasedAgent Tracks agents in a user's library. | Field | Type | Description | | --------------- | ------- | ----------------------------- | | `user_id` | UUID | FK to users | | `agent_id` | UUID | FK to marketplace\_agents | | `purchase_type` | String | free, purchased, subscription | | `is_active` | Boolean | Whether the license is active | ### ProjectAgent Assigns agents to specific projects. | Field | Type | Description | | ------------ | ------- | --------------------------------------- | | `project_id` | UUID | FK to projects | | `agent_id` | UUID | FK to marketplace\_agents | | `enabled` | Boolean | Whether agent is active on this project | ### AgentReview / BaseReview User reviews and ratings for agents and bases. | Field | Type | Description | | ----------------------- | ------- | ----------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `agent_id` or `base_id` | UUID | FK to the reviewed item | | `rating` | Integer | 1 to 5 | | `comment` | Text | Review text | ### AgentCoInstall Recommendation system data ("People also installed"). | Field | Type | Description | | ------------------ | ------- | -------------------------- | | `agent_id` | UUID | FK to marketplace\_agents | | `related_agent_id` | UUID | FK to marketplace\_agents | | `co_install_count` | Integer | Number of co-installations | ## Deployment Models ### Deployment External deployment history and status. | Field | Type | Description | | --------------------- | ------ | --------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `provider` | String | vercel, netlify, cloudflare | | `deployment_url` | String | Live deployment URL | | `status` | String | deploying, deployed, failed | | `error` | Text | Error message if failed | | `deployment_metadata` | JSON | Provider-specific metadata | ### DeploymentCredential Encrypted OAuth tokens for deployment providers. | Field | Type | Description | | ------------------------ | ------ | ------------------------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `project_id` | UUID | FK to projects (optional) | | `provider` | String | vercel, netlify, cloudflare, supabase | | `access_token_encrypted` | String | Fernet-encrypted token | | `provider_metadata` | JSON | Provider-specific metadata | ### GitProviderCredential Unified Git provider OAuth credentials. | Field | Type | Description | | ------------------- | ------ | ------------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `provider` | String | github, gitlab, bitbucket | | `access_token` | String | Encrypted access token | | `provider_username` | String | Username on the provider | ## Billing Models ### MarketplaceTransaction Revenue from agent purchases and usage. | Field | Type | Description | | ----------------- | ------- | ---------------------- | | `id` | UUID | Primary key | | `amount_total` | Integer | Total amount in cents | | `amount_creator` | Integer | Creator's share (90%) | | `amount_platform` | Integer | Platform's share (10%) | | `payout_status` | String | pending, paid, failed | ### CreditPurchase Credit package purchases. | Field | Type | Description | | ----------------------- | ------- | -------------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `amount_cents` | Integer | Purchase amount | | `credits_amount` | Integer | Credits received | | `stripe_payment_intent` | String | Stripe reference | | `status` | String | pending, completed, failed | ### UsageLog Token usage tracking for billing. | Field | Type | Description | | --------------- | ------- | ---------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `model` | String | LLM model used | | `tokens_input` | Integer | Input tokens consumed | | `tokens_output` | Integer | Output tokens consumed | | `cost_total` | Integer | Total cost in cents | ### UserAPIKey User API keys for BYOK (Bring Your Own Key) providers. | Field | Type | Description | | ----------------- | ------ | ------------------------------------ | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `provider` | String | openrouter, openai, groq, z-ai, etc. | | `encrypted_value` | String | Encrypted API key | ### UserCustomModel User-added models under BYOK providers. | Field | Type | Description | | ---------------------------------- | ------ | -------------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `model_id` | String | Model identifier | | `model_name` | String | Display name | | `provider` | String | Parent BYOK provider | | `pricing_input` / `pricing_output` | Float | Per-token pricing | ## Kanban Models Defined in `orchestrator/app/models_kanban.py`. ### KanbanBoard One board per project. | Field | Type | Description | | ------------ | ------ | -------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `name` | String | Board name | | `settings` | JSON | Board settings | ### KanbanColumn Customizable columns (Backlog, To Do, In Progress, Done). | Field | Type | Description | | -------------- | ------- | ---------------------------------- | | `id` | UUID | Primary key | | `board_id` | UUID | FK to kanban\_boards | | `name` | String | Column name | | `position` | Integer | Sort order | | `is_backlog` | Boolean | Whether this is the backlog column | | `is_completed` | Boolean | Whether this is the done column | | `task_limit` | Integer | WIP limit (optional) | ### KanbanTask Individual tasks with rich metadata. | Field | Type | Description | | ------------- | -------- | ---------------------- | | `id` | UUID | Primary key | | `column_id` | UUID | FK to kanban\_columns | | `title` | String | Task title | | `description` | Text | Task description | | `priority` | String | Priority level | | `task_type` | String | Task category | | `assignee_id` | UUID | FK to users (optional) | | `due_date` | DateTime | Due date (optional) | | `tags` | JSON | Tag list | ### KanbanTaskComment Collaboration comments on tasks. | Field | Type | Description | | --------- | ---- | ------------------- | | `id` | UUID | Primary key | | `task_id` | UUID | FK to kanban\_tasks | | `user_id` | UUID | FK to users | | `content` | Text | Comment content | ### ProjectNote Rich text notes using TipTap editor. | Field | Type | Description | | ---------------- | ------ | ------------------------- | | `id` | UUID | Primary key | | `project_id` | UUID | FK to projects | | `content` | Text | Note content | | `content_format` | String | Content format identifier | ## Feedback Models ### FeedbackPost User bug reports and feature suggestions. | Field | Type | Description | | -------------- | ------- | ----------------- | | `id` | UUID | Primary key | | `user_id` | UUID | FK to users | | `type` | String | bug or suggestion | | `title` | String | Post title | | `status` | String | Current status | | `upvote_count` | Integer | Total upvotes | ### FeedbackUpvote / FeedbackComment Upvotes (one per user per post) and comments on feedback posts. ## Source Files | File | Contents | | ----------------------------------- | --------------------------------------------------------------------- | | `orchestrator/app/models.py` | Main models (39 classes) | | `orchestrator/app/models_auth.py` | User, OAuthAccount, AccessToken | | `orchestrator/app/models_kanban.py` | KanbanBoard, KanbanColumn, KanbanTask, KanbanTaskComment, ProjectNote | | `orchestrator/app/schemas.py` | Pydantic request/response schemas | | `orchestrator/app/database.py` | Async SQLAlchemy engine and session setup | | `orchestrator/alembic/versions/` | Migration files | # Troubleshooting Source: https://docs.tesslate.com/developer/troubleshooting Common issues and solutions for developers and self-hosters ## Overview This page collects the most common issues encountered when developing, deploying, and self-hosting OpenSail. Each section includes symptoms, diagnosis commands, root causes, and solutions. If you are new to the codebase, scan the section headers to find the category that matches your problem. ## Container Issues ### Devserver Image Missing **Symptoms:** User project containers fail to start. Pods stuck in `ImagePullBackOff` or `ErrImagePull`. **Diagnosis:** ```bash theme={null} # Kubernetes: check which image is being requested kubectl describe pod -n proj- | grep Image # Check backend environment variable kubectl exec -n tesslate deployment/tesslate-backend -- env | grep K8S_DEVSERVER # Docker: check if the image exists locally docker images | grep tesslate-devserver ``` **Root cause:** The `tesslate-devserver` image was never built or loaded into the cluster. **Solution:** ```bash theme={null} docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/ ``` ```bash theme={null} docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/ minikube -p tesslate image load tesslate-devserver:latest ``` ```bash theme={null} docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/ docker tag tesslate-devserver:latest .dkr.ecr.us-east-1.amazonaws.com/tesslate-devserver:latest docker push .dkr.ecr.us-east-1.amazonaws.com/tesslate-devserver:latest ``` ### ImagePullBackOff **Symptoms:** Pod stuck in `ImagePullBackOff` state. **Diagnosis:** ```bash theme={null} kubectl describe pod -n # Look for "Failed to pull image" in Events ``` **Common causes and solutions:** 1. **Image not loaded into cluster (Minikube):** Run `minikube -p tesslate image load :latest` 2. **ECR credentials expired (AWS):** Re-authenticate: `aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com` 3. **Wrong image name in config:** Verify `K8S_DEVSERVER_IMAGE` in the backend environment matches the actual image name ### Pod Stuck in CrashLoopBackOff **Symptoms:** Pod repeatedly crashes and restarts. **Diagnosis:** ```bash theme={null} # Check current pod status kubectl get pods -n # Check pod events kubectl describe pod -n # Check container logs (current and previous) kubectl logs -n kubectl logs -n --previous ``` **Common causes:** 1. **Missing environment variables:** Verify secrets are properly mounted: `kubectl exec -n tesslate deployment/tesslate-backend -- env | grep DATABASE` 2. **Database connection failure:** Check `DATABASE_URL` and ensure the database pod is running 3. **Missing Python dependencies:** Rebuild the image with `--no-cache` ### Namespace Stuck in Terminating **Symptoms:** A project namespace stays in `Terminating` state and never completes deletion. **Diagnosis:** ```bash theme={null} kubectl get ns | grep proj- kubectl get all -n proj- ``` **Solution:** Force-delete the namespace by removing its finalizers: ```bash theme={null} kubectl get ns proj- -o json | \ jq '.spec.finalizers = []' | \ kubectl replace --raw "/api/v1/namespaces/proj-/finalize" -f - ``` Force-deleting a namespace skips finalizer cleanup. Ensure no critical resources (like PVCs with important data) are left orphaned. ## Database Issues ### Connection Refused **Symptoms:** Backend logs show `Connection refused` or `timeout` errors for PostgreSQL. **Diagnosis:** ```bash theme={null} # Docker: check postgres container docker compose ps postgres # Kubernetes: check postgres pod kubectl get pods -n tesslate | grep postgres kubectl logs -n tesslate deployment/tesslate-postgres ``` **Common causes:** 1. **Database not running:** Restart it: `docker compose up -d postgres` or `kubectl rollout restart deployment/tesslate-postgres -n tesslate` 2. **Wrong DATABASE\_URL:** Verify the format: `postgresql+asyncpg://user:pass@host:5432/dbname`. Check with: `kubectl exec -n tesslate deployment/tesslate-backend -- env | grep DATABASE_URL` 3. **Network policy blocking:** Ensure the NetworkPolicy allows backend-to-database traffic ### Migration Errors **Symptoms:** `alembic upgrade head` fails. **Diagnosis:** ```bash theme={null} cd orchestrator alembic current # Show current revision alembic history # Show migration history ``` **Common issues and solutions:** ``` alembic: ERROR: Multiple heads detected ``` Two developers created migrations from the same revision. Merge them: ```bash theme={null} alembic merge heads -m "merge_heads" alembic upgrade head ``` ``` relation "tablename" does not exist ``` Run all pending migrations: ```bash theme={null} alembic upgrade head ``` If a migration fails midway, check the current state and fix manually: ```bash theme={null} alembic current # If the migration was already applied manually, stamp it: alembic stamp ``` Ensure all model files are imported in `alembic/env.py`: ```python theme={null} from app.database import Base from app import models from app import models_kanban from app import models_auth ``` ### Database Seeding Failures **Symptoms:** Seed scripts fail or produce no data. **Diagnosis:** ```bash theme={null} # Check if migrations have been applied docker exec tesslate-orchestrator alembic current # Run seed script with verbose output docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed_marketplace_bases.py ``` **Common causes:** 1. **Migrations not applied:** Run `alembic upgrade head` first 2. **Script not copied into container:** Verify the `docker cp` step completed successfully 3. **PYTHONPATH not set:** Always include `-e PYTHONPATH=/app` when running scripts inside the container ## Agent Issues ### LLM Timeout or No Response **Symptoms:** Chat messages do not get responses. The UI spins indefinitely. **Diagnosis:** ```bash theme={null} # Check backend logs for chat/agent errors kubectl logs -n tesslate deployment/tesslate-backend | grep -i "chat\|agent\|litellm" # Check LiteLLM configuration kubectl exec -n tesslate deployment/tesslate-backend -- env | grep LITELLM ``` **Common causes:** 1. **Missing API key:** Verify `LITELLM_API_BASE` and `LITELLM_MASTER_KEY` are set 2. **Rate limiting:** Check logs for rate limit errors; implement exponential backoff 3. **Model not available:** Verify the model name in `LITELLM_DEFAULT_MODELS` is correct and accessible ### Tool Execution Failures **Symptoms:** Agent tool calls fail. Logs show tool execution errors. **Diagnosis:** ```bash theme={null} kubectl logs -n tesslate deployment/tesslate-backend | grep -i "tool\|execute" ``` **Common causes:** 1. **Container not running:** The user project container must be started before the agent can execute file or shell operations 2. **File path issues:** Tool file paths are relative to the project root; verify the expected file exists 3. **Permission denied:** Check that the container user has write access to the target directory ### Streaming Errors **Symptoms:** Agent responses cut off mid-stream or the SSE connection drops. **Common causes:** 1. **Proxy timeout:** NGINX Ingress default timeouts may be too short for long agent runs. Ingress annotations should set `proxy-read-timeout` and `proxy-send-timeout` to 3600 2. **Client-side EventSource disconnect:** Ensure the frontend properly handles reconnection 3. **Backend exception during streaming:** Check backend logs for tracebacks during the stream ## Deployment Issues (External Providers) ### SSL Certificate Not Valid **Symptoms:** Browser shows certificate warning when accessing the application. **Diagnosis:** ```bash theme={null} # Check certificate status kubectl get certificate -n tesslate kubectl describe certificate tesslate-wildcard-tls -n tesslate # Check cert-manager logs kubectl logs -n cert-manager deployment/cert-manager --tail=50 ``` **Common causes:** 1. **DNS not propagated:** Wait up to 48 hours for DNS propagation 2. **Cloudflare API token invalid:** The token needs Zone:Zone:Read and Zone:DNS:Edit permissions 3. **Wildcard cert subdomain limitation:** `*.domain.com` only covers one level. `foo.bar.domain.com` requires a separate cert or Cloudflare proxy ### Domain Routing (503 Service Unavailable) **Symptoms:** Browser shows 503 error when accessing the application or a user project. **Diagnosis:** ```bash theme={null} # Check pod readiness kubectl get pods -n tesslate # Check service endpoints kubectl get endpoints -n tesslate # Check ingress controller logs kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail=50 ``` **Solutions:** 1. **Pod not ready:** Wait for the pod to pass readiness checks, or check why it is failing 2. **Service endpoint stale:** Restart the ingress controller: `kubectl rollout restart deployment/ingress-nginx-controller -n ingress-nginx` 3. **Ingress misconfigured:** Inspect with `kubectl describe ingress -n tesslate` ### CORS Errors **Symptoms:** Browser console shows `Access to fetch has been blocked by CORS policy`. **Solutions:** 1. Verify `APP_DOMAIN` in backend config matches your frontend origin 2. Check the `DynamicCORSMiddleware` in `main.py` includes the correct URL patterns 3. Ensure both HTTP and HTTPS origins are allowed if your setup uses both ## Docker Issues ### Image Not Updating After Rebuild **Symptoms:** Code changes do not appear after rebuilding and redeploying. **Root cause:** Docker (and Minikube) caches images and does not overwrite existing images with the same tag. **Solution (Minikube):** ```bash theme={null} # 1. Delete old image from Minikube minikube -p tesslate ssh -- docker rmi -f tesslate-backend:latest # 2. Rebuild with --no-cache docker rmi -f tesslate-backend:latest docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/ # 3. Load to Minikube minikube -p tesslate image load tesslate-backend:latest # 4. Delete pod to force restart kubectl delete pod -n tesslate -l app=tesslate-backend ``` ### Volume Permission Errors **Symptoms:** Container fails to read or write files. Logs show "Permission denied." **Common causes:** 1. **Wrong user inside container:** Ensure the container user (1000:1000) owns the project files 2. **Host filesystem permissions:** On Linux, Docker volumes may inherit restrictive host permissions 3. **Windows line endings:** Files created on Windows may cause script execution failures inside Linux containers ### Network Conflicts **Symptoms:** Containers cannot communicate. Port conflicts on the host. **Diagnosis:** ```bash theme={null} docker network ls | grep tesslate docker port ``` **Solutions:** 1. Stop conflicting services on the host that use the same ports (5432, 8000, 5173) 2. Ensure the project network is connected to Traefik: check the Compose file for network configuration ## Kubernetes Issues ### Minikube Image Caching **Problem:** `minikube image load` does not overwrite existing images with the same tag. **Solution:** Always delete the old image before loading the new one: ```bash theme={null} minikube -p tesslate ssh -- docker rmi -f :latest minikube -p tesslate image load :latest ``` ### NGINX Ingress Configuration **Problem:** Ingress returns 503 or the wrong backend. **Diagnosis:** ```bash theme={null} kubectl get ingress -n tesslate -o yaml kubectl describe ingress -n tesslate kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail=50 ``` **Common fixes:** 1. Restart the ingress controller after backend deployments: `kubectl rollout restart deployment/ingress-nginx-controller -n ingress-nginx` 2. Verify service selectors match pod labels 3. Check that the ingress class annotation matches your controller ### PVC Not Bound **Symptoms:** Pod stuck in `Pending` with event "unbound PersistentVolumeClaims." **Diagnosis:** ```bash theme={null} kubectl get pvc -n proj- kubectl describe pvc project-storage -n proj- ``` **Common causes:** 1. **StorageClass not found:** Verify `K8S_STORAGE_CLASS` matches an available StorageClass: `kubectl get sc` 2. **No available PersistentVolumes:** The dynamic provisioner may not be configured 3. **Pod affinity violation:** All pods sharing a RWO PVC must be on the same node. Check for affinity constraint failures ### VolumeSnapshot Hibernation Failures **Symptoms:** Hibernation fails with "snapshot not ready" or "snapshot creation failed." **Diagnosis:** ```bash theme={null} # Check backend logs kubectl logs -n tesslate deployment/tesslate-backend | grep -i snapshot # Check VolumeSnapshot status kubectl get volumesnapshot -n proj- kubectl describe volumesnapshot -n proj- # Check snapshot controller kubectl logs -n kube-system -l app=snapshot-controller ``` **Common causes:** 1. **VolumeSnapshotClass not configured:** Ensure `tesslate-ebs-snapshots` exists: `kubectl get volumesnapshotclass` 2. **EBS CSI driver not installed:** The snapshot feature requires the AWS EBS CSI driver with snapshot support 3. **PVC does not exist or is not bound:** Verify the PVC is in `Bound` state before attempting a snapshot ## Quick Diagnostic Commands ```bash theme={null} # Overall cluster health kubectl get pods --all-namespaces kubectl get events --all-namespaces --sort-by=.lastTimestamp | tail -20 # Tesslate-specific kubectl get pods -n tesslate -o wide kubectl logs -n tesslate deployment/tesslate-backend --tail=100 kubectl logs -n tesslate deployment/tesslate-frontend --tail=100 # User project namespaces kubectl get pods --all-namespaces | grep proj- kubectl get ingress --all-namespaces | grep proj- # Resource usage kubectl top pods -n tesslate kubectl top nodes # Network kubectl get svc -n tesslate kubectl get endpoints -n tesslate ``` ## Getting Help If you cannot resolve an issue: ```bash theme={null} kubectl get pods -n tesslate -o yaml > pods.yaml kubectl logs -n tesslate deployment/tesslate-backend --tail=500 > backend.log kubectl describe pods -n tesslate > describe.txt ``` Search for the error message in the source code. Many errors have comments explaining the root cause and fix. Include: steps to reproduce, expected vs. actual behavior, relevant logs, configuration, and environment details (Minikube/AWS, versions). # FAQ Source: https://docs.tesslate.com/faq Common questions about OpenSail: licensing, pricing, models, data privacy, deployment, and usage Tesslate OpenSail ## General OpenSail is an open platform for building, running, and sharing AI-powered software. You describe a job, and OpenSail helps turn it into a working agent, app, scheduled automation, webhook handler, or MCP tool. Agents write code, use connected tools, remember context, and run in sandboxed environments. Key capabilities: * Natural-language agent coding with a real IDE (Monaco, terminal, git) * Multi-container projects with live preview * btrfs-based workspaces with instant snapshots and forking * 22+ deployment targets and 6+ messaging channels * Marketplace for agents, skills, MCP connectors, and installable apps * Runs on any model via LiteLLM Yes. OpenSail is Apache 2.0 licensed. You can: * Use it for personal or commercial projects * Self-host on your own infrastructure * Modify, fork, and redistribute the source You only pay for: * Infrastructure (if you deploy to a cloud) * AI API usage, or use free local models via Ollama or vLLM * Optional credit purchases on the hosted service at tesslate.com | Path | Who runs it | Data location | When to pick | | -------------------- | -------------------- | -------------------------------- | ---------------------------------------------- | | Cloud (tesslate.com) | Tesslate | Tesslate infrastructure | Zero setup, managed updates | | Desktop | You, on your machine | Local disk under `OPENSAIL_HOME` | Single user, fully offline capable | | Self-hosted | You, on your infra | Your servers | Team deployments, data sovereignty, air-gapped | The desktop app can also pair to a cloud instance (ours or your own self-hosted) to get remote sandboxed compute while keeping projects local. Yes. Apache 2.0 allows commercial use, paid products, client work, selling hosting services, and proprietary extensions. No restrictions on users, revenue, or business type. ## Models and AI All model calls route through LiteLLM, so anything LiteLLM supports works out of the box. That includes: * Anthropic (Claude Sonnet, Opus, Haiku) * OpenAI (GPT-4, GPT-4o, reasoning models) * Google (Gemini Pro, Flash) * DeepSeek, Qwen, Mistral, Meta, Moonshot, MiniMax, Z.AI, xAI * OpenRouter as a meta-provider * Self-hosted via Ollama, vLLM, or any OpenAI-compatible endpoint Configure available models with `LITELLM_DEFAULT_MODELS`. See the [model management guide](/guides/model-management). Not necessarily. Three options: 1. BYOK: attach your own key from OpenAI, Anthropic, OpenRouter, Groq, Together, DeepSeek, Fireworks, or anyone else. Pay your provider directly. 2. Local models: run Ollama or vLLM for zero AI cost and full offline operation. 3. Hosted credits on tesslate.com. Yes. Two supported paths: * Desktop with Ollama: the Tauri app talks to a local model server over HTTP. No network required. * Self-hosted with a local LiteLLM pointed at vLLM, Ollama, or any OpenAI-compatible endpoint. Features that require network (marketplace, cloud pairing, external deploys) are gracefully degraded when offline. ## Deployment No. Three supported modes selected by `DEPLOYMENT_MODE`: * `desktop`: SQLite + local subprocess or Docker runtime. Best for single users. * `docker`: Docker Compose with Postgres, Redis, Traefik. Best for single-server installs and dev. * `kubernetes`: per-project namespaces, btrfs CSI, Volume Hub. Best for multi-tenant production. Start on Docker, migrate to Kubernetes when you need isolation, hibernation, or horizontal scaling. Anywhere that can run Docker or Kubernetes. Tested paths: * Local: laptop, home server, dev box * Cloud: AWS (EC2/EKS), GCP (GCE/GKE), Azure (VM/AKS), DigitalOcean, Hetzner, Linode * On-prem: company datacenter, private cloud, air-gapped networks See [AWS production](/guides/aws-production) and [Kubernetes on Minikube](/guides/kubernetes-local). Yes. Set `APP_DOMAIN=opensail.example.com`, `APP_PROTOCOL=https`, `COOKIE_DOMAIN=.opensail.example.com`. Projects become `{container}.{project-slug}.opensail.example.com` under Kubernetes and `{container}.localhost` under Docker. TLS is handled by Traefik + Let's Encrypt (Docker) or cert-manager + Cloudflare DNS (Kubernetes). Minimum (Docker): 8 GB RAM, 10 GB disk, Docker Engine + Compose v2. Recommended dev: 16 GB RAM, 20 GB disk. Production Kubernetes: two or more btrfs-capable nodes with 4 vCPU / 8 GB RAM each, NGINX Ingress, managed Postgres, managed Redis, S3 bucket. Desktop: 4 GB RAM, 2 GB disk, macOS 12+, Windows 10+, or modern Linux. ## Storage and workspaces Each project lives on a btrfs subvolume managed by the OpenSail btrfs CSI driver and the Volume Hub orchestrator. Subvolumes support instant snapshot-clone, which enables: * Fork a running workspace in seconds * Roll back to any snapshot in the per-project timeline (up to 5) * Hibernate and restore a full multi-container project atomically Object persistence uses content-addressed storage on S3 (or MinIO in dev). See the [architecture page](/self-hosting/architecture). Projects auto-hibernate after `K8S_HIBERNATION_IDLE_MINUTES` of inactivity (default 10). The Volume Hub triggers an S3 CAS sync, then the pods are torn down. On next use, `EnsureCached` brings the volume back: fast path if still cached on a node, otherwise peer-transfer from another node, otherwise restore from S3. Multi-container projects hibernate and restore atomically because they share a volume and pod affinity pins them to one node. Yes. Every project is a git repo inside its workspace. Push to any remote, clone elsewhere, or `kubectl cp`/`docker cp` the files out. Apps can also be exported as a CAS bundle via the publish pipeline. ## Privacy and security When self-hosted: everything stays on your infrastructure. No telemetry. The only network calls are to AI providers you configure and to marketplaces or deployment targets you connect. On tesslate.com: projects live on Tesslate infrastructure. Prompts go to the configured AI provider. We do not sell or share customer data. No. Agents are sandboxed: * Docker/K8s: agents run inside the project container and can only see files in that container * Desktop `local` runtime: agents see only the project directory under `OPENSAIL_HOME` (or a symlinked adopted folder) * Shell commands run in the project container * Tools gated by `.tesslate/permissions.json` require approval for sensitive actions * Passwords: bcrypt hashed in Postgres * OAuth tokens, API keys, channel credentials, deployment credentials: Fernet encrypted at rest * 2FA codes: Argon2 hashed, 5-attempt cap, short TTL * Desktop `tsk_` keys: Tauri Stronghold vault (encrypted on disk) See [Authentication](/authentication). Self-hosted is neutral: compliance depends on your infrastructure. The hosted service at tesslate.com follows SOC 2 Type II controls. Contact [support@tesslate.com](mailto:support@tesslate.com) for a compliance pack. ## Pricing and billing Self-hosted: you pay your own cloud and AI bills. OpenSail itself is free. Hosted (tesslate.com): tiered subscriptions plus optional credit packs. Stripe handles checkout. Marketplace creators get paid via Stripe Connect on a configurable creator/platform revenue split. For self-hosted Stripe setup, configure `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and tier price IDs. See the [billing guide](/guides/billing). Agent runs are bounded by `AGENT_MAX_COST`, `AGENT_MAX_COST_PER_RUN`, and `AGENT_MAX_ITERATIONS`. Container tiers are bounded by `COMPUTE_MAX_CONCURRENT_PODS`. MCP installs per user cap at `MCP_MAX_SERVERS_PER_USER`. All are configurable. ## Building and publishing Build your project in a workspace, then publish via the Architecture Panel. The publisher: 1. Serializes the workspace to a manifest and CAS bundle 2. Runs the staged approval pipeline (stage0 to stage3) for public listings 3. Creates an immutable `AppVersion` Private and team installs skip the public listing gate. See [Publishing apps](/guides/publishing-apps). Configure a channel under `Settings -> Channels`. Each channel (Slack, Telegram, Discord, WhatsApp, Signal) stores Fernet-encrypted credentials. The Gateway v2 runner maintains a persistent connection, routes messages to the right agent, and delivers responses back to the platform. Schedules can fire at cron intervals with per-schedule delivery targets. See [Communication gateways](/guides/communication-gateways). The platform ships React/TypeScript bases, plus community bases for Next.js, Vite + FastAPI, Vite + Go, Django, Rails, Laravel, .NET, Flutter, Expo, and more. Agents can generate code in any language; containers run whatever the base specifies. Yes. Options: * Git import: paste a GitHub/GitLab/Bitbucket URL * Base template: pick a pre-wired stack * Describe: let the agent scaffold the stack * Desktop: adopt any existing folder via symlink or marker file ## Troubleshooting Refresh the preview panel. Check the dev server container logs (`docker compose logs ` or `kubectl logs -n proj- `). If hot reload is broken on mounted volumes, set `CHOKIDAR_USEPOLLING=true`. Verify `LITELLM_API_BASE` is reachable and `LITELLM_MASTER_KEY` is correct. Check the model selector shows valid entries. Look at the orchestrator logs for LiteLLM errors and per-user budget rejections. Check Traefik is running (`docker compose ps`). On systems that do not auto-resolve `*.localhost`, add `127.0.0.1 studio.localhost` to your hosts file. Run `kubectl describe pod -n proj-` and check events. Common causes: missing `K8S_DEVSERVER_IMAGE`, PVC still provisioning, image pull secret wrong. Verify the Volume Hub is healthy in `kube-system`. ## Getting help Full documentation. Source and issue tracker. Community support. Direct support. # Agents Source: https://docs.tesslate.com/guides/agents The OpenSail agent runner is tesslate-agent, a 33-tool Python package that drives any LiteLLM-compatible model through a streaming iteration loop with progressive persistence and context compaction. Tesslate OpenSail ## The agent runner OpenSail ships one first-class agent runner: [**tesslate-agent**](https://github.com/TesslateAI/tesslate-agent). It is a Python package vendored in the monorepo at `packages/tesslate-agent/` and also published standalone. One class (`TesslateAgent`), 33 tools across 8 categories, LiteLLM for model access, and ATIF v1.4 trajectory recording. There is no longer a `StreamAgent`, `IterativeAgent`, or `ReActAgent`. All four were replaced by this single runner. File ops, shell, navigation, git, memory, web, planning, delegation. OpenAI, Anthropic, DeepSeek, Qwen, Gemini, Mistral, xAI, Z.AI, local Ollama, any OpenAI-compatible endpoint. Every step streams to the database as it happens. Pods can die mid-run, you can resume later. At 80 percent of the model window, the agent compacts older messages with a cheap model and keeps going. ## The agent loop Everything happens inside `TesslateAgent.run(user_request, context)`. It is an async generator that yields events and never touches the filesystem directly. Every tool call goes through the `ToolRegistry`. ``` loop (iteration = 1..N): pre-flight compaction if tokens >= window * 0.80 response = model.chat_with_tools(messages, tools) if response.content: yield stream event if not response.tool_calls: yield complete and return append assistant turn with tool_calls execute tool_calls in parallel (respecting approval policy) append tool results ``` The loop ends when the model emits a turn with no tool calls. There is no fixed iteration cap. Runs can be hours long thanks to compaction and progressive persistence. Full protocol, tool schemas, edit modes, and quirks live in the agent repo: [DOCS.md on GitHub](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). ## Tool catalog 33 tools across 8 categories. The registry is fixed; extensions happen through skills and MCP bridges, not new tool modules. `read_file`, `write_file`, `read_many_files`, `patch_file`, `multi_edit`, `apply_patch`, `view_image`, `file_undo`. Surgical edits use `patch_file` with fuzzy matching. Atomic multi-file changes use `multi_edit`. `apply_patch` takes unified diffs. `file_undo` rolls back the most recent edit. `bash_exec`, `shell_open`, `shell_close`, `shell_exec`, `write_stdin`, `list_background_processes`, `read_background_output`, `python_repl`. Shell sessions are persistent PTY sessions. Long-running processes (dev servers, watchers) go to background and the agent reads their output on demand. `glob`, `grep`, `list_dir`. Fast code search without cat-ing entire files. `git_log`, `git_blame`, `git_status`, `git_diff`. Read-only. Writes go through shell. `memory_read`, `memory_write`. Persistent key-value store scoped to the agent run. `web_fetch`, `web_search`. Tavily, Brave, or DuckDuckGo backends configured at the platform level. `update_plan`. Structured plan that lives alongside the chat and gets updated as work progresses. `task`, `wait_agent`, `send_message_to_agent`, `close_agent`, `list_agents`. Spawn subagents for focused subtasks and coordinate between them. OpenSail extends this set with platform tools for project control, graph editing (`apply_setup_config`), and kanban (TSK-NNNN refs). The core 33 ship with every install. ## Subagents and delegation An agent can spawn subagents for focused subtasks. Each subagent gets its own system prompt, its own tool subset, and its own budget. The parent agent sees subagent output and decides what to do next. Typical patterns: * **Divide and conquer** Frontend agent and backend agent run in parallel inside the same workspace * **Specialist lookup** Spawn a `doc-writer` subagent to generate changelog entries * **Verification** Spawn a `reviewer` subagent to audit the main agent's diff before finalizing Subagents use the `task`, `wait_agent`, `send_message_to_agent`, `close_agent`, and `list_agents` tools. They run in the same workspace, so file edits are visible to everyone. ## Skills and progressive disclosure Skills are reusable capabilities (writing style, code review checklist, deployment playbook, research methodology). They attach to any agent. Skills load progressively: 1. The lightweight **catalog** (name + description) is injected into the agent's context 2. The full **body** is pulled on demand only when the agent calls `load_skill` This keeps the context window lean. An agent with 20 skills attached still only carries 20 short descriptions until it needs one. Built-in skills like `project-architecture` use live marker tokens (`{{MARKER}}`) that get resolved at load time with the current config schema, service catalog, and URL patterns, so skill bodies never drift out of sync with the platform. ## MCP integration The agent supports MCP (Model Context Protocol) natively. Installed MCP servers appear as native tools in the agent's registry. Tool schemas are cached in Redis (`MCP_TOOL_CACHE_TTL`, default 300 seconds) and refreshed on OAuth or server update. The MCP bridge also surfaces: * **Citations** returned by MCP tools render as `CitationCard` in chat * **Re-auth prompts** trigger a yellow `ReauthBanner` linking to `/settings/connectors` * **Structured outputs** per MCP spec 2025-06-18+ ## Context compaction When a session crosses 80 percent of the active model's context window, the agent compacts older messages with a cheap model (`compaction_summary_model`, usually a small, fast model). Compaction preserves tool outputs by reference and keeps the most recent turns verbatim. The agent continues without hitting a wall. Multi-hour runs are normal. Compaction is automatic and costless from the user's perspective. ## Progressive persistence Every agent step (`AgentStep`) streams to the database as it happens. The row goes in before the next tool call. If the pod dies, the browser closes, or the network hiccups, you can come back later and see the full trajectory. Resuming picks up from the last persisted step. No re-execution, no duplicate tool calls. ## Edit modes and approval Edit mode controls how much autonomy the agent has. Switch it any time from the chat input. Dangerous tools (writes, shell, git push) trigger an `ApprovalRequestCard` in chat. Three buttons: Allow Once, Allow All, Stop. Read-only tools run without prompting. All tools run immediately. Use when you trust the agent fully. Read-only. The agent gathers context and produces a structured plan instead of mutating anything. Activate with the mode button or `/plan`. On desktop, permissions also flow through `.tesslate/permissions.json` per project. Each capability (shell, network, git push, file write, process spawn) has an `allow`, `deny`, or `ask` policy with optional always-allow persistence. ## Orchestration backends `tesslate-agent` has a pluggable orchestration layer. `LocalOrchestrator` ships in the open-source repo. OpenSail registers Docker, Kubernetes, and desktop-local backends via `OrchestratorFactory.register(mode, cls)`, so the same agent code runs against subprocesses, containers, or pods without change. ## Running an agent From the chat UI, pick an agent and type. From the external API, call `POST /api/external/agent/invoke` with your `tsk_` API key and subscribe to SSE events at `/api/external/agent/events/{task_id}`. From the CLI (inside the `tesslate-agent` repo), run `tesslate-agent run --task "..." --workdir . --output trajectory.json`. Every execution produces an ATIF v1.4 trajectory recording (`trajectory.json`). Use it for replay, debugging, and benchmark analysis. ## Related Every tool, every parameter, every quirk. How agents stream to the chat panel. Install specialized agents, skills, and MCP servers. Invoke agents from outside the UI with `tsk_` keys. # API Keys Source: https://docs.tesslate.com/guides/api-keys Two kinds of keys in OpenSail: external keys for invoking agents from outside, and BYOK provider keys for model routing. Both encrypted at rest. Tesslate OpenSail ## Two kinds of keys OpenSail uses two distinct kinds of API keys, and it's important to know which one you need. For calling OpenSail agents from outside the platform: Slack bots, CI pipelines, cron jobs, external integrations. `tsk_` prefixed. Created in Settings. For using your own OpenAI, Anthropic, OpenRouter, or other provider keys instead of platform credits. Encrypted at rest. Pro and Ultra only. This page covers both. If you're wiring up a Slack bot, you want external API keys. If you're trying to bypass platform credit costs by using your own OpenAI key, you want BYOK. For BYOK specifics, see `/guides/model-management`. ## External API keys External keys let outside systems invoke OpenSail agents through a REST API. The canonical flow: a Slack bot mentions your agent, a CI job kicks off a code review, a cron schedule fires a weekly report, an external coding agent (Cursor, Claude Code) reaches into your OpenSail instance for sandboxed compute. ### Key format * Prefix: `tsk_` * Body: 32 random hex characters * Example: `tsk_8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c` ### How storage works Raw keys are never stored. When you create a key: 1. The server generates 32 random hex characters 2. Prepends `tsk_` to form the full token 3. Computes SHA-256 of the full token 4. Stores only the hash plus a 4-character prefix for identification 5. Returns the raw key to you **once** You cannot retrieve the raw key later. If you lose it, rotate it. ### Creating a key Settings, then the API Keys section. You'll see tabs for external keys and BYOK keys. Click Create Key. Give it a human-readable name ("Slack Bot", "CI Deploy Hook"). Optionally restrict the key to a single project by setting a `project_id`. Scoped keys can only invoke agents on that project. Unscoped keys can access any project you own. Optionally set `expires_at`. After that timestamp, the key stops authenticating. The raw key is shown one time. Copy it into your secret manager or the integration you're wiring up. You cannot view it again. Treat `tsk_` keys like passwords. Never commit them to git. Use your platform's secret store (env vars, Vault, AWS Secrets Manager, etc.). ### Using a key Pass the raw key as a Bearer token: ``` Authorization: Bearer tsk_8f3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c ``` The most common endpoint is the agent invoke endpoint: ```bash theme={null} curl -X POST https://your-opensail.example.com/api/external/agent/invoke \ -H "Authorization: Bearer tsk_..." \ -H "Content-Type: application/json" \ -d '{ "project_id": "550e8400-e29b-41d4-a716-446655440000", "message": "Review the open PR and post a summary in Slack", "webhook_url": "https://my-service.example.com/hooks/opensail" }' ``` The response comes back immediately: ```json theme={null} { "task_id": "arq:task:abc123", "chat_id": "660e8400-e29b-41d4-a716-446655440000", "events_url": "/api/external/agent/events/arq:task:abc123", "status": "queued" } ``` ### Reading results Three options, pick whichever fits your integration. Open a Server-Sent Events connection to `events_url`. You get live tool calls, text, and the final response as the agent works. ```bash theme={null} curl -N -H "Authorization: Bearer tsk_..." \ https://your-opensail.example.com/api/external/agent/events/arq:task:abc123 ``` If the connection drops, reconnect with `?last_event_id=evt_002` to replay missed events before resuming live stream. For simple webhooks and CI pipelines where SSE is impractical: ```bash theme={null} curl -H "Authorization: Bearer tsk_..." \ https://your-opensail.example.com/api/external/agent/status/arq:task:abc123 ``` Response: ```json theme={null} { "status": "completed", "response": "I reviewed the PR and posted a summary to #engineering.", "iterations": 3, "tool_calls": 5, "started_at": "2026-04-23T10:00:01Z", "completed_at": "2026-04-23T10:00:15Z" } ``` Statuses: `queued`, `running`, `completed`, `failed`, `cancelled`. Pass `webhook_url` in the invoke payload. When the task completes, OpenSail POSTs the final status to your URL. Your endpoint receives the same payload shape as the polling response. Good for: CI pipelines, serverless workflows, systems that can't hold an open connection. ### Scopes and rate limits | Setting | Behavior | | -------------------- | ----------------------------------------------- | | `project_id` scope | Key can only invoke agents on the named project | | `expires_at` | Key stops authenticating after this timestamp | | Active keys per user | Capped at 10 | | `is_active` | Soft-delete; revoked keys cannot be reactivated | Rate limits apply per key to prevent runaway loops. Defaults are generous but enforced. ### Rotation and revocation Generate a new key in Settings. Copy the raw value. Update your integration to use the new key. Verify calls succeed. Back in Settings, click Revoke on the old key. `is_active` flips to false; the key stops authenticating immediately. Keys can also be revoked via `DELETE /api/external/keys/{key_id}`. ## BYOK provider keys BYOK (Bring Your Own Key) lets you route AI calls through your own provider key instead of paying platform credits. Available on Pro and Ultra subscription tiers. When the agent picks a model whose provider matches a key you have stored, LiteLLM routes the call through your key. Cost is zero in platform credits; you pay the upstream provider directly. The `UsageLog` row is still created for your analytics with `is_byok=True` and `billed_status="exempt"`. Supported: OpenAI, Anthropic, OpenRouter (covers 100+ models), Google, DeepSeek, Groq, Together, Fireworks, or any OpenAI-compatible endpoint. See `/guides/model-management` for the full list and self-hosted options. ### Adding a provider key Sign in at the provider console (platform.openai.com, console.anthropic.com, openrouter.ai, etc.) and generate an API key. The BYOK tab lists every provider key you have stored (values masked). Pick the provider, paste the key, optionally name it. The key is Fernet-encrypted at rest. Raw keys are never logged or cached. Start an agent session with a matching model. The model selector shows a BYOK badge and the session does not deduct platform credits. ### Where keys are stored Each BYOK key is a `UserAPIKey` row: | Field | Purpose | | ----------------- | -------------------------------------------------------------------- | | `provider` | Provider identifier (`openai`, `anthropic`, `openrouter`, etc.) | | `auth_type` | `api_key`, `oauth_token`, `bearer_token`, or `personal_access_token` | | `key_name` | Your label | | `encrypted_value` | Fernet-encrypted raw key | | `is_active` | Soft-deleted keys remain in the table for audit | | `last_used_at` | Timestamp of most recent use | ### BYOK detection When you invoke a model, `is_byok_model(model_name)` checks the provider prefix against `BUILTIN_PROVIDERS`. If the prefix matches one of your stored keys, the call runs BYOK. Adding a new provider to the registry automatically enables BYOK routing for its models. BYOK only applies when a matching key exists for your user. If you have an Anthropic key but pick a DeepSeek model, the DeepSeek call uses platform credits. Keys don't cross providers. ## Security External key hashes are SHA-256. BYOK keys are encrypted with Fernet using a server-held key (`ENCRYPTION_KEY`). Raw values are never persisted in plaintext. Tool outputs are scrubbed for high-entropy strings before being returned to the model. Your keys never leak into the transcript or an exported trajectory. Rotate external keys whenever a team member changes roles. Rotate BYOK keys on any suspicion of compromise and whenever the provider recommends it. External keys bind to a user, not to a service account. If a user leaves, revoke their external keys. Plan for handover: critical integrations should use service-owned user accounts. ## Common patterns Generate an external key scoped to one project. Configure your Slack app to POST mentions to a worker. The worker calls `/api/external/agent/invoke` with the mention text. Use the webhook callback to post the agent's response back into Slack. Generate a short-lived external key (with `expires_at`). Use it in a GitHub Actions workflow to call `/api/external/agent/invoke` on PR events. Poll the status endpoint until complete, then post the agent's response as a PR comment. Use OpenSail's built-in scheduler (see `/guides/using-agents`) instead of an external cron. The scheduler invokes agents on a cron expression with full context. External keys are for when you must trigger from outside. Cursor, Claude Code, or any MCP-capable agent can talk to OpenSail's MCP server using an external key. The agent gets sandboxed compute, access to your project tools, and can publish apps back to OpenSail. ## Troubleshooting Key expired, revoked, or mis-pasted. Check the raw value has no leading whitespace or trailing newline. Verify the key is still active in Settings. Key is scoped to a different project. Either create a new key for the target project or use an unscoped key. ARQ worker pods may be saturated. Check worker health. For long queues, tune `worker_max_jobs` in the platform config. Verify the model's provider prefix matches your stored key's provider. A Claude model call only uses an Anthropic key, not an OpenAI key. Check BYOK is enabled for your tier (Pro or Ultra). ## Next steps BYOK routing, passthrough mode, self-hosted models How BYOK zeros your credit cost while keeping analytics Invoke an agent from the chat UI instead of the API Wire Slack, Gmail, and more into the agent itself instead of bolting external callers on top # Architecture Panel Source: https://docs.tesslate.com/guides/architecture-panel One canvas, one config file, two authors. The Architecture Panel is a React Flow graph that renders .tesslate/config.json. Humans and agents edit the same source of truth. Tesslate OpenSail ## What it is The Architecture Panel is a visual node-graph canvas where you design, wire, and manage the full topology of a project. Every project has one. It is the single source of truth for what your app is: which containers run, how they connect, where secrets flow, where it deploys. Under the hood it is a React Flow canvas backed by `.tesslate/config.json`. Both humans and agents read and write the same file. When the agent adds a Postgres container and wires its `DATABASE_URL` into the backend, new nodes and edges appear on the canvas in real time. When you drag a new service onto the canvas, the agent sees the updated graph on its next iteration. One file, two authors, no drift. Drag, drop, connect. No docker-compose YAML to hand-edit. Agents call `apply_setup_config` to mutate the graph through the same schema. Parseable, roundtrippable, versionable. Agents reason over structure, not prose. The manifest you publish as an app is a serialized snapshot of this graph. ## Node types Every node on the canvas is one of four types. Your app containers (frontend, backend, workers). Color-coded by role: * **Green** base (user application code) * **Blue** service (Postgres, Redis, MySQL, Mongo, MinIO) * **Purple** external (Supabase, Stripe, SendGrid) * **Cyan** hybrid Each shows status (running, stopped, starting, error), port, and tech stack. Click to open the properties panel. Double-click to jump into the code editor. Live iframe windows rendered directly on the canvas. Resizable, draggable, with back/forward/home/refresh buttons and a URL bar. You can watch your running app while you wire its architecture. Preview nodes accept at most one incoming connection (the container they mirror). Branded cards for each provider (Vercel, Netlify, Cloudflare, AWS, 22 total). Shows environment tags (production, staging, preview), connected containers, deployment history, and the live URL after a successful deploy. Click the env tag to cycle production/staging/preview. A/B deployments work naturally: connect the same container to two targets. The TesslateLLM proxy node. Represents an agent contained inside an app: the creator configures the system prompt, bound tools, bound MCPs, and model preference. At runtime the node resolves to a shared worker pool with per-session keys tied to the installer's wallet. Used heavily in app authoring. ## Edge types Edges express different kinds of dependency. Each has its own color and animation so the graph stays readable as it grows. | Edge type | Color | Style | Meaning | | ----------------- | ------ | ----------------- | --------------------------------------------------- | | `env_injection` | Orange | Dashed | Source container's exports become target's env vars | | `http_api` | Blue | Solid, animated | HTTP service dependency | | `database` | Green | Solid | Persistence dependency | | `cache` | Red | Dashed | Redis or memcached | | `browser_preview` | Purple | Dashed | Container to preview window | | `deployment` | Orange | Dashed with arrow | Container to deployment target | | `agent_invokes` | Gray | Solid | Hosted agent to service dependency | Draw an edge by dragging from any of a node's four handles to another node. The canvas validates connection compatibility (preview nodes accept one incoming, deployment targets accept only static and server containers, and so on). ## Why a graph The AI agent needs a structured, parseable, roundtrippable target. If "what are the containers and how do they connect" lives as free-form prose in chat, every edit requires re-inferring state. The panel gives both humans and agents a typed graph they can read and write. Benefits stack up: * **Credentials and secrets are visible as env\_injection edges**, not buried in `.env` files * **Multi-container topology is first-class** instead of hidden inside docker-compose YAML * **Previews are explicit nodes** so you always know what the iframe is pointing at * **Deployments are explicit nodes** so A/B deploys are a drawing, not a config change * **Publishing serializes the graph into the app manifest** so install restores the same graph into a new project ## Editing Three surfaces mutate the graph: Pull a node from the Marketplace sidebar. Draw edges with the mouse. Reposition nodes. All changes persist to `.tesslate/config.json` through the `sync_project_config` pipeline. Click any container node. The right-side panel opens with name, image, port, env vars, and startup command. Save and the graph updates. Agents mutate the graph with the `apply_setup_config` tool. This validates every startup command, upserts Container rows, full-replaces ContainerConnection, DeploymentTarget, and BrowserPreview records, and writes the file. All in one transaction. Never edit `.tesslate/config.json` through `write_file`. Writes through `write_file` skip container sync, swallow validation errors, and drift the database out of sync with the file. Always use `apply_setup_config`. ## Auto-layout The canvas auto-lays nodes on initial load using Dagre (left-to-right). Click **Auto Layout** to re-apply after big changes. Node sizes are hinted: containers 180x100, browser previews 320x280, deployment targets 220x120. ## Canvas powers * **Pan and zoom** with cursor-anchored math so the point under the cursor stays fixed * **Snap guides** auto-align nodes as you drag * **Lock toggle** prevents accidental edits while navigating * **Theme-aware** styling uses `var(--surface)`, `var(--text)` so the graph adapts to dark/light and custom themes * **Memoized nodes and edges** keep rendering smooth even on 50+ node graphs ## Relationship to apps When you publish a workspace as an app, the Architecture Panel's graph becomes the app's manifest. Containers, connections, deployment targets, and hosted agents all ship together. When someone installs your app, the install saga restores the graph into a new project with their volume, their credentials, and their billing configuration. One edit to the canvas in the source workspace is one diff in the next version. ## Related The project that owns the graph. Preview nodes on the canvas. All 22 supported targets. Serialize the graph into a manifest. # AWS Production (EKS) Source: https://docs.tesslate.com/guides/aws-production Deploy Tesslate OpenSail to AWS EKS with ECR, S3, IAM roles, NGINX Ingress, cert-manager, Cloudflare DNS, the btrfs CSI driver, and Volume Hub. Tesslate OpenSail This guide walks through a production deployment of OpenSail on AWS EKS. It covers the shared platform stack, per-environment provisioning, image builds, secrets, DNS and TLS, database seeding, scaling, observability, and rollbacks. If you have not yet run OpenSail on a local Kubernetes cluster, start with the [Local Kubernetes](/guides/kubernetes-local) guide first. The cloud topology is the same; AWS just adds managed nodes, ECR, and real TLS. Every `kubectl` command in this guide includes `--context=tesslate-production-eks` or `--context=tesslate-beta-eks`. Never use `kubectl config use-context` or any context-switching helper: cronjobs and other processes can flip your active context mid-session, and an accidental `kubectl delete` against the wrong cluster is unrecoverable. ## 1. What you will deploy Kubernetes 1.35 on managed node groups (on-demand plus spot), OIDC provider for IRSA, control-plane logs to CloudWatch. Repositories in account **859561299901** (us-east-1) for `tesslate-backend`, `tesslate-frontend`, `tesslate-devserver`, `tesslate-ast`, `tesslate-btrfs-csi`, and seeded app images. Encrypted bucket for project hibernation and CAS. IRSA roles for backend, EBS CSI, cluster-autoscaler, external-dns, cert-manager. AWS Network Load Balancer fronts NGINX Ingress. All HTTP traffic terminates here before hitting workloads. Let's Encrypt wildcard via DNS01 against Cloudflare. Terraform creates the apex and `*.domain` records on the NLB hostname. Per-project btrfs subvolumes with CAS sync to S3 and Volume Hub orchestration. Same stack as local minikube. Per-environment stacks additionally provision a VPC (`10.0.0.0/16`, three public plus three private subnets across three AZs), a NAT gateway, LiteLLM (with optional RDS backend), and the application workloads in the `tesslate` namespace (backend, frontend, worker, Redis, Postgres or external RDS, cleanup CronJobs). The **shared stack** (`k8s/terraform/shared/`) provisions cross-environment resources: ECR repos, a small `tesslate-platform-eks` cluster for internal tools (Headscale VPN and friends), and platform-level NGINX Ingress plus cert-manager plus Cloudflare DNS. ## 2. Prerequisites AWS CLI v2. `aws sts get-caller-identity` must succeed against the target account. Provider pinning handled in `main.tf`. State is stored in S3, keyed per environment. kubectl matching EKS 1.35. Helm v3 for running cert-manager or ingress charts manually. Needed for `linux/amd64` builds. Apple Silicon still works because buildx cross-compiles. Token scoped to `Zone:DNS:Edit` and `Zone:Zone:Read` on the target zone. IAM user in `eks_admin_iam_arns`, or a named team role with push permissions. IAM requirements: * Ability to create VPC, EKS, IAM, S3, and ECR resources (direct or via assumed role). * An IAM user listed in `eks_admin_iam_arns` for the target environment, or membership in a team IAM group. See the [EKS access model](#4-eks-access) section for details. * AWS Secrets Manager access to `tesslate/terraform/{production,beta,shared}` for pulling tfvars. Windows / MSYS: prefix `kubectl` and `docker exec` with `MSYS_NO_PATHCONV=1` so Git Bash does not rewrite paths. ## 3. First-time provisioning Always apply the shared stack first. The per-environment stack references ECR image URLs built in the shared stack; without ECR, image pulls fail during the first rollout. ```bash theme={null} ./scripts/terraform/secrets.sh download shared ./scripts/aws-deploy.sh init shared ./scripts/aws-deploy.sh plan shared ./scripts/aws-deploy.sh apply shared ``` Creates ECR repositories, the platform EKS cluster, platform NGINX Ingress plus cert-manager, and Cloudflare DNS hosted zone wiring. ```bash theme={null} ./scripts/terraform/secrets.sh download beta ./scripts/aws-deploy.sh terraform beta # init + plan + apply ``` Exercise the full pipeline on beta before touching production. Per-environment applies take 15 to 20 minutes on the first run. ```bash theme={null} ./scripts/terraform/secrets.sh download production ./scripts/aws-deploy.sh terraform production ``` Subsequent applies that only change secrets or Helm values complete in under two minutes. The `aws-deploy.sh` helper auto-detects backend drift: if your local `.terraform/terraform.tfstate` points at the wrong environment, it reinitializes with the correct backend HCL before running plan or apply. ## 4. Environments: beta vs production | Field | Value | | --------------------- | ------------------------- | | Terraform state key | `beta/terraform.tfstate` | | Backend config | `backend-beta.hcl` | | tfvars file | `terraform.beta.tfvars` | | Secrets Manager entry | `tesslate/terraform/beta` | | Kustomize overlay | `k8s/overlays/aws-beta/` | | ECR tag | `:beta` | | kubectl context | `tesslate-beta-eks` | | Field | Value | | --------------------- | ------------------------------- | | Terraform state key | `production/terraform.tfstate` | | Backend config | `backend-production.hcl` | | tfvars file | `terraform.production.tfvars` | | Secrets Manager entry | `tesslate/terraform/production` | | Kustomize overlay | `k8s/overlays/aws-production/` | | ECR tag | `:production` | | kubectl context | `tesslate-production-eks` | The two environments are fully isolated: separate VPCs, EKS clusters, state files, and tfvars. Most access and day-two operations pivot on the kubectl context. ## 5. Secrets management (envFrom auto-sync) Three Kubernetes secrets in the `tesslate` namespace are fully terraform-managed from `k8s/terraform/aws/kubernetes.tf`: * `tesslate-app-secrets`: app-level config (`APP_DOMAIN`, `LITELLM_MASTER_KEY`, OAuth client secrets, Stripe keys, SMTP, PostHog, etc.) * `postgres-secret`: Postgres credentials * `s3-credentials`: S3 bucket config (the backend pod uses IRSA for auth, so no static AWS keys land in the secret) The backend, frontend, and worker Deployments mount these via `envFrom`. This is the auto-sync half of the pattern: **every key added to a terraform-managed secret is available as a pod env var on the next rollout, with no kustomize edit required**. The other half is explicit `env` entries in `k8s/overlays/aws-base/backend-patch.yaml`. Those entries live under a `$patch: replace` directive so the base manifest's env array is wiped and only static values plus one alias (`K8S_INGRESS_DOMAIN` to `APP_DOMAIN`) remain. Without `$patch: replace`, stale base entries would merge back in. **Decision rule.** Secret-managed value (domain, API key, OAuth client secret, Stripe key, SMTP password): add to terraform's `kubernetes.tf` secret. Static config (feature flag, class name, pod affinity toggle, replica count patch): add to the overlay in `k8s/overlays/aws-base/backend-patch.yaml` or an environment-specific patch. ### Rotating a secret ```bash theme={null} ./scripts/terraform/secrets.sh download production # edit k8s/terraform/aws/terraform.production.tfvars ./scripts/aws-deploy.sh plan production ./scripts/aws-deploy.sh apply production ./scripts/terraform/secrets.sh upload production ./scripts/aws-deploy.sh reload production backend worker ``` The `reload` step rolls pods so they pick up the new secret values. ## 6. EKS access EKS uses a **role-based model**. Regular humans assume one of four team roles. Terraform, CI, and a small list of named admins (`tesslate-terraform`, `tesslate-bigboss`) assume the `eks-deployer` role. | I want to... | Role | ARN pattern | | --------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------- | | `kubectl logs`, `get`, `describe`, read CloudWatch logs, browse ECR | **team-observer** | `arn:aws:iam::859561299901:role/tesslate-{env}-eks-team-observer` | | Above, plus `kubectl rollout`, restart/patch deployments, push to ECR | **team-deployer** | `arn:aws:iam::859561299901:role/tesslate-{env}-eks-team-deployer` | | Above, plus `kubectl exec`, shell into pods, run debug containers | **team-debugger** | `arn:aws:iam::859561299901:role/tesslate-{env}-eks-team-debugger` | | Above, plus Secrets Manager, RBAC, namespace mgmt, IAM team users | **team-admin** | `arn:aws:iam::859561299901:role/tesslate-{env}-eks-team-admin` | Run this once per machine. `--role-arn` bakes role assumption into the resulting kubeconfig, so every later `kubectl` call uses it. ```bash theme={null} aws eks update-kubeconfig \ --region us-east-1 \ --name tesslate-production-eks \ --alias tesslate-production-eks \ --role-arn arn:aws:iam::859561299901:role/tesslate-production-eks-eks-deployer aws eks update-kubeconfig \ --region us-east-1 \ --name tesslate-beta-eks \ --alias tesslate-beta-eks \ --role-arn arn:aws:iam::859561299901:role/tesslate-beta-eks-eks-deployer ``` ```bash theme={null} kubectl --context=tesslate-production-eks get nodes kubectl --context=tesslate-beta-eks get nodes ``` Team members who are not in `eks_admin_iam_arns` should use named AWS CLI profiles with `role_arn` entries for the team role they need. Full onboarding flow (IAM groups, `~/.aws/config` snippets, assume-role one-liner) lives in the [EKS Cluster Access](https://github.com/TesslateAI/opensail/blob/main/docs/guides/eks-cluster-access.md) guide. `aws-deploy.sh` invokes `aws eks update-kubeconfig` under the hood with `--role-arn arn:aws:iam::859561299901:role/tesslate-{env}-eks-eks-deployer` every time it touches the cluster, so you do not have to rerun the commands above for its subcommands. ## 7. Build and push images Six images live in ECR under account **859561299901** in `us-east-1`: | Repository | Dockerfile | Purpose | | ------------------------------------------ | ----------------------------------- | ------------------------------------- | | `tesslate-backend` | `orchestrator/Dockerfile` | FastAPI + ARQ worker | | `tesslate-frontend` | `app/Dockerfile.prod` | React + Vite SPA behind NGINX | | `tesslate-devserver` | `orchestrator/Dockerfile.devserver` | User project container base | | `tesslate-ast` | `services/ast/Dockerfile` | AST parser sidecar of the backend pod | | `tesslate-btrfs-csi` | `services/btrfs-csi/Dockerfile` | CSI driver + Volume Hub | | `tesslate-markitdown`, `tesslate-deerflow` | `seeds/apps/.../Dockerfile` | Seeded Tesslate Apps | Tag convention: `:production` or `:beta` for first-class images; `:latest` for seeded app images. ```bash Build and push via helper theme={null} # All core images: build, push, apply manifests, roll deployments ./scripts/aws-deploy.sh build production # Single image ./scripts/aws-deploy.sh build production backend ./scripts/aws-deploy.sh build beta frontend # Multiple images in parallel ./scripts/aws-deploy.sh build beta backend frontend worker # Reuse Docker cache (default is --no-cache) ./scripts/aws-deploy.sh build beta backend --cached # Volume Hub and CSI driver ./scripts/aws-deploy.sh build production compute ``` ```bash Manual build and push theme={null} aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin 859561299901.dkr.ecr.us-east-1.amazonaws.com docker buildx build --platform linux/amd64 --no-cache \ -t 859561299901.dkr.ecr.us-east-1.amazonaws.com/tesslate-backend:production \ -f orchestrator/Dockerfile . --push ``` The `build` subcommand performs these steps: 1. `git submodule update --init --recursive` (the agent runner in `packages/tesslate-agent` is copied into the backend image). 2. `aws ecr get-login-password | docker login` against `859561299901.dkr.ecr.us-east-1.amazonaws.com`. 3. `docker buildx build --platform linux/amd64 --push` in parallel across selected images. 4. `aws eks update-kubeconfig` with the `eks-deployer` role for the target environment. 5. `kubectl apply -k k8s/overlays/aws-{env}` to pick up any manifest changes. 6. Rolling restart of the impacted Deployments plus a parallel `kubectl rollout status --timeout=300s`. 7. If the backend was rebuilt, `python -m scripts.seed_apps` runs inside the backend pod to upsert the Tesslate Apps registry. Always build with `--no-cache` for deploys, and buildx with `--platform linux/amd64`. Cached `pip`/`npm` layers, or an `arm64` build from Apple Silicon, cause pods to start with stale code or fail with `no match for platform in manifest`. ## 8. Deploy ```bash Deploy via helper theme={null} ./scripts/aws-deploy.sh deploy-k8s production ./scripts/aws-deploy.sh deploy-compute production # btrfs CSI + Volume Hub ``` ```bash Deploy via kubectl theme={null} kubectl apply -k k8s/overlays/aws-production --context=tesslate-production-eks kubectl --context=tesslate-production-eks rollout status deployment/tesslate-backend -n tesslate --timeout=300s kubectl --context=tesslate-production-eks rollout status deployment/tesslate-frontend -n tesslate --timeout=300s kubectl --context=tesslate-production-eks rollout status deployment/tesslate-worker -n tesslate --timeout=300s ``` ```bash Targeted restarts after a config change theme={null} ./scripts/aws-deploy.sh reload production # applies manifests, rolls backend/frontend/worker ./scripts/aws-deploy.sh reload production backend worker # rolls only those two ./scripts/aws-deploy.sh reload production litellm # syncs LiteLLM ConfigMap and rolls LiteLLM ./scripts/aws-deploy.sh reload production volume-hub # rolls Volume Hub in kube-system ``` ## 9. Verify ```bash theme={null} # Cluster reachable, nodes Ready kubectl --context=tesslate-production-eks get nodes # Core pods Ready kubectl --context=tesslate-production-eks get pods -n tesslate -o wide # CSI driver and Volume Hub kubectl --context=tesslate-production-eks get pods -n kube-system \ -l 'app in (tesslate-btrfs-csi-node,tesslate-volume-hub)' # Ingress exposed through the NLB kubectl --context=tesslate-production-eks get ingress -A # NLB DNS hostname kubectl --context=tesslate-production-eks get svc -n ingress-nginx ingress-nginx-controller \ -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' # Public health probe curl -sI https://opensail.tesslate.com/api/health ``` Log in through the browser at `https:///` and confirm the dashboard loads and you can create a project. ## 10. DNS and TLS DNS and certificates are fully managed by terraform plus in-cluster controllers: * **Cloudflare DNS records** (`dns.tf`) create CNAMEs for the apex domain and `*.domain` pointing at the NLB hostname, proxied through Cloudflare. * **external-dns** reconciles per-project subdomain records from Ingress annotations when users deploy preview projects. * **cert-manager** runs a `ClusterIssuer` that uses the Cloudflare API token for DNS01 challenges, minting a wildcard Let's Encrypt cert stored in the `tesslate-wildcard-tls` Secret. Ingress resources reference it via `K8S_WILDCARD_TLS_SECRET`. * **Cloudflare SSL mode** should be `Full (strict)` so browser to edge and edge to NLB are both encrypted. Check certificate state: ```bash theme={null} kubectl --context=tesslate-production-eks get certificate -n tesslate kubectl --context=tesslate-production-eks describe certificate tesslate-wildcard-tls -n tesslate kubectl --context=tesslate-production-eks logs -n cert-manager deploy/cert-manager --tail=100 ``` The first issuance takes two to five minutes. If `Ready=False` for longer than ten minutes, check `kubectl describe certificaterequest` and cert-manager logs for Cloudflare API errors. The frontend's `api-url` in the `frontend-config` ConfigMap (managed in terraform's `kubernetes.tf`) must be the base domain only, for example `https://opensail.tesslate.com`. `app/src/lib/api.ts` already prefixes `/api` to every path; putting `/api` in the base URL causes double `/api/api/` paths and universal 404s. ## 11. Seed the production database Run this once after the initial terraform apply. Seeds upsert by slug, so running twice is safe but wasteful. ```bash theme={null} CTX=tesslate-production-eks kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_marketplace_bases kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_marketplace_agents kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_opensource_agents kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_skills kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_mcp_servers kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seeds.seed_themes kubectl --context=$CTX exec -n tesslate deploy/tesslate-backend -- \ python -m scripts.seed_apps ``` Run database migrations before seeds on every version bump: `alembic upgrade head` inside the backend pod. ## 12. Scaling Three independent layers: `k8s/overlays/aws-production/replicas-patch.yaml` sets backend, frontend, worker, and ingress controller replica counts. Hotfix scale: `kubectl --context=tesslate-production-eks scale deploy/tesslate-backend -n tesslate --replicas=4`. metrics-server is installed by default (`enable_metrics_server = true`). Add `HorizontalPodAutoscaler` per Deployment as needed. Installed via IRSA. On-demand node group scales between `eks_node_min_size` and `eks_node_max_size`; spot up to `eks_spot_max_size`. User project workloads prefer the spot group. To add a dedicated node group (GPU, memory-optimized), set `additional_node_groups` in tfvars and apply. Schema lives in `variables.tf`. The worker Deployment defaults to a single replica because in-memory task state is still partially colocated with the process. Scale it cautiously and watch Redis queue depth via the worker logs. ## 13. Observability * **Control plane logs** stream to CloudWatch at `/aws/eks/tesslate-{env}-eks/cluster`: ```bash theme={null} aws logs tail /aws/eks/tesslate-production-eks/cluster --since 15m ``` * **Workload logs**: `kubectl --context=tesslate-production-eks logs -n tesslate deploy/tesslate-backend -f`. * **Metrics**: `kubectl --context=tesslate-production-eks top pods -n tesslate` and `kubectl top nodes`. For historical data, install `kube-prometheus-stack` via Helm or route metrics from the OpenTelemetry Collector to CloudWatch. * **Structured logs + OpenTelemetry**: see the [Enterprise observability guide](https://github.com/TesslateAI/opensail/blob/main/docs/guides/enterprise-observability.md) for deploying the OTel Collector, wiring exporters, and enabling the audit log stream. ## 14. Updates and migrations Normal PR flow. CI runs tests and linters. Run the `Deploy Production` workflow (`.github/workflows/deploy-production.yml`) in GitHub Actions. It downloads `tesslate/terraform/production` from Secrets Manager, runs `terraform plan -detailed-exitcode`, applies, then runs `./scripts/aws-deploy.sh deploy-k8s production` followed by `./scripts/aws-deploy.sh build production`. Or, from your workstation: ```bash theme={null} ./scripts/aws-deploy.sh build production ``` ```bash theme={null} kubectl --context=tesslate-production-eks exec -n tesslate deploy/tesslate-backend -- \ alembic upgrade head ``` Run this before the rolling restart finishes, otherwise new pods may crash on boot trying to use an older schema. Pods roll one at a time on `rollout restart`, so the Deployment is always serving traffic. For a hard cutover or a very large schema migration, drain traffic first using the safe shutdown procedure. ## 15. Rollback and safe shutdown Rollback a single Deployment: ```bash theme={null} kubectl --context=tesslate-production-eks rollout history deploy/tesslate-backend -n tesslate kubectl --context=tesslate-production-eks rollout undo deploy/tesslate-backend -n tesslate kubectl --context=tesslate-production-eks rollout status deploy/tesslate-backend -n tesslate --timeout=300s ``` Rollback a bad image tag: repush the previous known-good digest as `:production`, or bump `newTag` in `k8s/overlays/aws-production/kustomization.yaml` to a specific SHA and reapply. For planned downtime, maintenance windows, or draining user pods cleanly, follow the safe shutdown procedure in `docs/guides/safe-shutdown-procedure.md` on GitHub: stop user containers, scale the worker and backend to zero, pause the task queue, and only then apply the risky change. ## 16. Troubleshooting Your IAM user is not in a team IAM group or `eks_admin_iam_arns`. Assume a team role first (one-shot `aws sts assume-role`, or a named AWS profile with `role_arn`) before running any `aws eks` or `kubectl` command. Your kubeconfig was written without `--role-arn`, so it resolves to your raw user, which has no EKS access entry. Re-run `aws eks update-kubeconfig --region us-east-1 --name tesslate-production-eks --alias tesslate-production-eks --role-arn arn:aws:iam::859561299901:role/tesslate-production-eks-eks-deployer` **while** the assumed role credentials are active in your shell. The resulting kubeconfig bakes in the role. ECR login expired, wrong region, or wrong account. Refresh login and retry: ```bash theme={null} aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin 859561299901.dkr.ecr.us-east-1.amazonaws.com ./scripts/aws-deploy.sh build production backend ``` Image was built on arm64. Rebuild with `docker buildx build --platform linux/amd64 --push`. The `build` subcommand does this by default. Backend pod not Ready, or the ingress-nginx controller cache is stale after a backend restart. ```bash theme={null} kubectl --context=tesslate-production-eks get pods -n tesslate kubectl --context=tesslate-production-eks rollout restart deploy/ingress-nginx-controller -n ingress-nginx ``` Cloudflare API token missing DNS edit permission, or the zone is wrong. Verify the token has `Zone:Zone:Read` and `Zone:DNS:Edit` on the correct zone, and check cert-manager logs: ```bash theme={null} kubectl --context=tesslate-production-eks logs -n cert-manager deploy/cert-manager --tail=200 kubectl --context=tesslate-production-eks describe certificaterequest -n tesslate ``` The `api-url` ConfigMap includes a trailing `/api`. Set `frontend_api_url = "https://opensail.tesslate.com"` in tfvars (no `/api`) and reapply. Secret rotation broke a required key, or `tesslate-app-secrets` is missing a key consumed via `envFrom`. ```bash theme={null} kubectl --context=tesslate-production-eks describe pod -n tesslate -l app=tesslate-backend kubectl --context=tesslate-production-eks logs -n tesslate -l app=tesslate-backend --previous ``` Confirm the key exists in `k8s/terraform/aws/kubernetes.tf` and reapply. Git submodule was not initialized before `docker build`. Run `git submodule update --init --recursive`, then rebuild. The `build` script handles this automatically. The CSI DaemonSet rolled at the same time as Hub. Wait for `tesslate-btrfs-csi-node` rollout to stabilize; `./scripts/aws-deploy.sh build compute` and `reload volume-hub` sequence these correctly. A project was deleted before its namespace drained. ```bash theme={null} kubectl --context=tesslate-production-eks get ns | grep proj- kubectl --context=tesslate-production-eks delete ns proj- ``` cert-manager or external-dns is pending due to DNS. Apply with `-target=module.eks` first, then rerun the full apply once the cluster is Ready. ## Next steps Reproduce production locally on minikube for debugging storage, ingress, or snapshot issues. Fastest inner-loop dev flow. Useful when you do not need Kubernetes in the loop. Ship a Tesslate App to the marketplace from production. Configure Stripe, tiers, and credit accounting for the hosted experience. ## Getting help Real-time help from the Tesslate community. Source, issues, and release notes. Direct support at `support@tesslate.com`. # Billing Source: https://docs.tesslate.com/guides/billing Credits, subscription tiers, app billing dimensions, and creator payouts in OpenSail Tesslate OpenSail ## Overview OpenSail uses a multi-source credit system for AI usage, a four-tier subscription model for platform access, and a per-app, per-dimension billing engine for published apps on the marketplace. Payments run through Stripe with idempotent webhook fulfillment. Daily, bundled, signup bonus, and purchased, consumed in priority order Free, Basic, Pro, Ultra, with increasing limits and BYOK on Pro+ Three dimensions (AI, compute, platform fee), four modes per dimension 90% to creator, 10% platform, via Stripe Connect ## Credits Credits are the unified currency for AI usage. One credit equals one US cent. Costs are calculated in cents with Decimal arithmetic so no rounding drift compounds over long runs. ### The four credit pools | Pool | Source | Expiration | Priority | | ---------------- | --------------------------------------------- | ----------------------------------------- | -------------------- | | **Daily** | Free-tier allowance, 5 credits per day | Resets at UTC midnight | 1st (consumed first) | | **Bundled** | Monthly allowance from your subscription tier | Resets every 30 days on your billing date | 2nd | | **Signup bonus** | One-time gift at registration, 15,000 credits | Expires after 60 days | 3rd | | **Purchased** | Credit packages you buy | Never expire | 4th (consumed last) | Deduction priority is ordered to maximize the value of your purchased credits. Free pool gets used first, then bundled, then expiring bonus, then permanent purchased. ### How deduction works Before the agent calls the model, `check_credits(user, model_name)` verifies you have sufficient credits. BYOK routes always pass this check since no platform credits are charged. The model responds, token counts come back. `calculate_cost_cents(model, tokens_in, tokens_out)` looks up the current per-token prices (fetched live from LiteLLM, cached 5 minutes), multiplies, and rounds up with ceiling arithmetic. Non-zero usage always costs at least 1 cent. `deduct_credits` pulls from pools in priority order under a `SELECT FOR UPDATE` lock. Concurrent requests are serialized; the balance never goes negative. A `UsageLog` row records model, tokens, cost, agent, project, and BYOK status. ### Credit packages Buy credits that never expire: | Package | Credits | Price | | ------- | ------- | ----- | | Small | 500 | \$5 | | Medium | 2,500 | \$25 | | Large | 10,000 | \$100 | | Team | 50,000 | \$500 | Purchases complete through Stripe Checkout. Fulfillment is idempotent, so webhook replays don't double-credit. ## Subscription tiers | Feature | Free | Basic (\$20/mo) | Pro (\$49/mo) | Ultra (\$149/mo) | | ----------------------- | --------- | --------------- | ------------- | ---------------- | | Monthly bundled credits | 0 | 500 | 2,000 | 8,000 | | Daily credits | 5 | 0 | 0 | 0 | | Max projects | 3 | 7 | 15 | 40 | | Max deployments | 1 | 3 | 5 | 20 | | BYOK | No | No | Yes | Yes | | Support | Community | Email | Priority | Priority | | Annual billing | N/A | Available | Available | Available | Exact tier pricing and credit allocations are configured in the platform and can vary per deployment. Self-hosted instances can set their own tier structure. ### Upgrading The unified billing hub lives at Settings, Billing. Click Upgrade on the tier you want. Choose monthly or annual. You're redirected to Stripe's secure checkout. Card details never touch OpenSail servers. After payment succeeds, your tier updates, bundled credits are granted, the reset date is set 30 days out, and BYOK unlocks (if Pro or Ultra). ### Canceling Cancellation takes effect at the end of the current period. You keep all tier benefits until then. Purchased credits are never affected by tier changes; they persist forever. ## Team billing Teams have their own billing scope: a separate subscription tier, a separate credit balance, and a separate Stripe customer. Team admins manage billing at Settings, Team Billing. Individual team members use the team's credits when running agents on team-owned projects, and their personal credits for personal projects. For teams with billing admins separate from platform admins, the permissions system (`orchestrator/app/permissions.py`) scopes billing actions to the admin role. ## App billing: three dimensions, four modes Published apps on the Tesslate Apps marketplace have their own billing model, separate from platform credits. When a user installs your app, the creator decides who pays for what. ### The three dimensions All model calls the app's bundled agents make during execution. Priced per token, calculated through the same dynamic LiteLLM pricing used by the core credit system. Non-AI runtime costs: container time, file operations, snapshot storage. Billed based on active runtime and persistent volume usage. A percentage of total revenue that covers marketplace infrastructure, approval pipeline costs, and payout rails. Default is 10% (creators net 90%). ### The four modes per dimension Each dimension can be set to one of four modes, independently: | Mode | Who pays | When to use | | ----------------------------- | ----------------------------------------------------------------- | -------------------------------------- | | **Creator pays** | The app creator pays this dimension | Free-to-try apps, lead-gen apps, demos | | **Installer pays** | The user installing pays this dimension | Standard marketplace apps | | **Platform subsidized** | Tesslate covers this dimension | Featured apps, promotional launches | | **BYOK (bring your own key)** | Installer uses their own provider key, bypassing routing entirely | Power users, enterprise installations | A creator might set AI compute to "installer pays", general compute to "creator pays" (absorbing hosting), and the platform fee to "installer pays". Or BYOK for AI compute so enterprise customers route through their own keys, while the creator still absorbs general compute. ### Promotional budgets Creators can sponsor the first N installers by funding a promotional budget. While the fund lasts, all three dimensions are free to the installer. When the fund runs out, billing flips to whatever mode the creator configured. This is the go-to pattern for launches: free for the first 1,000 installers, then installer-pays. Installers get a taste, creators get distribution. ### Caps and overage Each dimension has its own caps and overage behavior configurable per app: * Hard cap: stop executing when the cap is hit * Soft cap: warn, prompt for an override, continue * No cap: run until the subscriber's credits are exhausted Caps protect both creators (from runaway installer usage when creator-pays) and installers (from runaway agents when installer-pays). ## Creator payouts | Recipient | Share | | --------- | ----- | | Creator | 90% | | Platform | 10% | Revenue share applies to everything a creator sells: marketplace agents, published apps, paid skills, paid connectors. One-time purchases, subscriptions, and API-metered usage all split the same way. Payouts go through Stripe Connect Express accounts. Creators onboard through a one-time Stripe flow (KYC, bank account, tax info). After onboarding, every sale that clears credits the creator's Stripe balance, which pays out to the creator's bank on Stripe's default schedule. Creators see earnings per app, per period, in Settings, Billing, Earnings. Stripe provides detailed transaction logs including payouts-in-flight. ## Usage tracking Settings, Billing. See total cost, tokens in/out, request count for the current billing period, broken down by model and by agent. Click into usage logs for per-request records with model, tokens, cost, agent, project, BYOK status, and timestamp. At 20% of your monthly allowance remaining, the header shows a warning badge. At 0 credits, an out-of-credits modal prompts you to purchase or upgrade. ## Stripe reliability OpenSail's Stripe integration is built for idempotency and reliability under webhook replay: * Every fulfillment path checks the `stripe_payment_intent` for duplicate processing * Verify-checkout on redirect is a safe no-op if the webhook already fulfilled * Subscription updates, tier changes, and cancellations all log to the audit table * Transfer and invoice creation include `idempotency_key` to prevent upstream replay If a payment succeeds in Stripe but your credits don't update within 30 seconds, check Transaction History. Support can reconcile from the Stripe session ID. ## Test cards (for self-hosted and dev) | Card | Behavior | | --------------------- | ------------------ | | `4242 4242 4242 4242` | Succeeds | | `4000 0000 0000 0002` | Declined | | `4000 0025 0000 3155` | Requires 3D Secure | | `4000 0000 0000 9995` | Insufficient funds | Use these to test the full checkout flow locally. ## Troubleshooting Card declined at the bank. Update payment method in Settings, Billing, or through the Stripe Customer Portal. Wait 30 seconds for the webhook to fire. Refresh the page (verify-checkout fallback). Check Transaction History for status. Contact support with the Stripe session ID if still missing. The daily reset loop runs hourly. Free-tier users reset at UTC midnight. Paid-tier users reset on their billing date; the Stripe webhook is the primary trigger, with a safety-net hourly sweep. Check usage by model in Settings, Billing, Usage. Switch heavy tasks to cheaper models (Haiku, Flash, Qwen-Coder). Add BYOK on Pro or Ultra to bypass credits entirely. Set per-run cost limits on specific agents. ## Next steps Pick cost-effective models and understand BYOK Add BYOK provider keys and external API keys Set up the three billing dimensions and promotional budgets Browse apps and see pricing in the wild # Chat Interface Source: https://docs.tesslate.com/guides/chat-interface The chat panel is where you drive the agent: streaming output, inline tool calls, approval cards, agent and model selectors, file picker, attachments, and a full session history. Tesslate OpenSail ## What the chat does The chat panel is your primary interface to the agent. You type what you want, the agent plans, calls tools, writes files, runs commands, and streams its work back in real time. Every tool call is visible, every dangerous write pauses for approval, and every session persists so you can pick up later. Chat ships in two surfaces: the project-builder sidebar (`ChatContainer`) and the standalone `/chat` page (not scoped to a project). Token-by-token between tool calls. See reasoning as it happens. Every tool invocation expands to show parameters, result, and duration. Dangerous writes surface an inline card: Allow Once, Allow All, Stop. Named sessions per project. Switch, rename, search, delete. ## Anatomy User and agent bubbles with markdown, GFM, code blocks, and syntax highlighting. Every agent message shows its `AgentStep` list inline (tool name, parameters, result summary, duration). Click a step to expand for full details. Message input with attachment picker, slash command dropdown, model and agent pills, edit-mode button, file-picker (`@filename`) dropdown, up/down arrow history, and chip mode when a slash command matches exactly. Session title, session popover (switch, rename, create, delete), project connector (for standalone chat), and the usage ribbon showing credits remaining. Standalone chat gets a dedicated left sidebar with the full session list. Project chat uses a popover invoked from the top bar. Both support search, new, rename, delete, and collapse. ## Agent and model selectors Dropdown that switches the active agent. Shows icon, name, model. Search filters the list. "Navigate to library" opens `/library?tab=agents` where you can install or create more. The selected agent persists per project in localStorage. Model picker rendered via `createPortal` so it escapes transform parents. Search, check for active model, caret for future sort options. Populated from `marketplaceApi.getModels`. Switching models mid-session is allowed. Compaction adapts automatically to the new context window. ## Edit modes The mode pill in the input cycles between three states. The active mode controls what tools run without prompting. Read-only tools run without interruption. Writes, shell commands, and git pushes surface an `ApprovalRequestCard`. Three buttons: * **Allow Once** run this call, prompt again next time * **Allow All** stop prompting for this tool for the rest of the session * **Stop** cancel the call and return control to the user Fully autonomous. All tools run immediately. Use when you trust the agent fully. Read-only. The agent gathers context and produces a structured markdown plan instead of mutating anything. Activate with the mode button or `/plan` slash command. ## Attachments Drop files into the input or click the attachment button. Supported: * **Images** PNG, JPG, WebP, GIF (agent uses vision-capable models to read them) * **Code and text files** any text MIME; content inlines in the next message * **Project files** type `@filename` and the file picker dropdown autocompletes from the project Attachment chips appear above the input. `AttachmentStrip` renders them with preview, filename, and remove buttons. Persisted attachments (from the database) and pre-upload attachments share a single component. ## Slash commands Built-ins: | Command | Action | | -------- | --------------------------------------------- | | `/clear` | Delete every message in the current session | | `/undo` | Undo the last user and agent exchange | | `/retry` | `/undo` then re-send the removed user message | Installed skills also appear as slash commands (for example `/docker-dev` if the `docker-dev` skill is attached). Commands disable while the agent is running or waiting for approval. ## Streaming events The chat consumes WebSocket and SSE events in a fixed protocol: | Event | Purpose | | ---------------------- | -------------------------------------------------------------- | | `text_delta` | Real-time token-by-token streaming between tool calls | | `agent_step` | A completed agent iteration (tool calls and result summary) | | `agent_task_started` | Execution kicked off from another origin (for example the API) | | `agent_task_completed` | Final response ready | | `agent_task_error` | Execution failed | | `approval_required` | Show an `ApprovalRequestCard` | | `approval_response` | Client reply to approval | | `complete` | Stream finished | Key invariants kept by the UI: * `text_delta` creates a streaming message with id `${thinkingMessageId}-stream` inserted before the thinking indicator; subsequent deltas append * Tool iteration messages use id `${thinkingMessageId}-iter-${iteration}` to deduplicate parallel tool calls * `isMountedRef` guards every setState inside WS/SSE callbacks, so the backend keeps running even after navigation ## Sessions Every project has its own chat sessions. Sessions keep their own history, their own active agent, and their own model selection. The `+` button in the session popover or sidebar creates a new session. Click the title to rename. Good names help you find them later. Use the popover (project chat) or the sidebar (standalone chat) to jump between sessions. Right-click (or the kebab menu) to delete. Confirmation required. Standalone chat (`/chat`) also has a `ProjectConnector` widget so you can attach a session to any project, inherit its context, and detach later. ## MCP integrations When an agent calls an MCP tool: * **Citations** Results tagged with `_mcp_structured.citation` render as `CitationCard` with external-link buttons * **Re-auth prompts** Tools that return `_mcp_reauth_required` trigger a yellow `ReauthBanner` linking to `/settings/connectors` * **Structured outputs** MCP spec 2025-06-18+ structured payloads render in a dedicated viewer ## Thinking indicator `TypingIndicator` animates three dots while the agent is streaming or running a tool. The indicator disappears as soon as the first `agent_step` completes for a given message. ## Tool debug modal Developers can invoke tools manually through `ToolDebugModal` (rendered via `createPortal`). Pick a tool, supply JSON parameters, see the result, timing, and errors. Useful for writing custom agents and testing new tool configurations. ## Copy and regenerate Every agent message has a copy button and a regenerate button. Regenerate re-runs the same user message from the previous turn with the same agent and model selection. Undo and retry are also available via slash commands. ## Usage and limits `UsageRibbon` sits at the top of chat with credit balance and a Get More CTA. When credits run out, inputs disable and the user is prompted to upgrade or add BYOK keys. BYOK-configured models bypass platform wallets entirely. ## Mobile On small viewports, the chat collapses to a bottom sheet with a drag handle. Full functionality is available, but multi-column layouts (editor + chat + preview) give way to one at a time. ## Related The agent loop that drives chat. The files the agent edits from chat. How `/docker-dev` and other slash-command skills work. MCP tool results rendering in chat. # Code Editor Source: https://docs.tesslate.com/guides/code-editor Monaco-based code editor with multi-language syntax, autocomplete, find-in-files, refactor support, and a file tree that stays in sync with the agent. Tesslate OpenSail ## A real editor OpenSail ships a full Monaco editor (the engine that powers VS Code), not a chat box with a side panel. You get the same syntax highlighting, autocomplete, command palette, multi-cursor, and refactor shortcuts you expect from a dedicated coding tool. The editor lives in `app/src/components/CodeEditor.tsx` and composes with the file tree, tab bar, and Design view. TypeScript, JavaScript, Python, Go, Rust, Ruby, Java, C#, SQL, HTML, CSS, Markdown, YAML, JSON, and more. IntelliSense via Monaco's built-in language services. TypeScript gets full type inference. In-file and project-wide. Regex, case-sensitive, whole-word. Rename symbol, extract function, go to definition, find references. ## One tree, two authors The agent sees the same tree, the same files, and the same shell output you do. Every edit the agent makes is a reviewable diff you can accept, reject, or keep editing. Every command the agent runs shows up in your terminal. The agent's work is your work, in the same checkout. Files are saved through `POST /api/projects/{slug}/files/save`. The save writes to the database (always), and to the filesystem (Docker) or the volume (Kubernetes). The file-manager pod syncs changes across container restarts. Agents and humans post to the same endpoint. ## File tree The left sidebar renders the project file tree with expand/collapse, single-click to open, right-click context menu (create file, create folder, rename, delete). * **Search** filter at the top narrows the tree as you type * **Empty directory support** directories with no files still render (backend sends entries ending in `/`) * **Excluded directories** like `node_modules`, `.git`, `.next`, `__pycache__`, `dist`, `build`, `.venv`, `.cache`, `.turbo`, `coverage` never appear * **Live updates** when the agent creates, renames, or deletes files (WebSocket push, not polling) ## Tabs Click a file to open it in a tab. Multiple tabs open at once. Close with the `x` or middle-click. Tabs track dirty state (unsaved changes get a dot). ## Autosave Files autosave after a short debounce when you stop typing. Toast confirmation ("Saved") appears briefly. No need to Cmd+S, but Cmd+S still works and triggers an immediate save. ## Language features Monaco detects language from the file extension. Supported out of the box: | Extension | Language | | ------------------------- | ---------------------- | | `.ts`, `.tsx` | TypeScript (JSX-aware) | | `.js`, `.jsx` | JavaScript | | `.py` | Python | | `.go` | Go | | `.rs` | Rust | | `.rb` | Ruby | | `.java` | Java | | `.cs` | C# | | `.sql` | SQL | | `.html`, `.css`, `.scss` | Web | | `.md`, `.mdx` | Markdown | | `.json`, `.yaml`, `.toml` | Config | | `.sh`, `.bash` | Shell | ## Keyboard shortcuts All the standard Monaco shortcuts work: | Shortcut | Action | | ---------------------- | --------------------------- | | `Cmd/Ctrl + S` | Save | | `Cmd/Ctrl + P` | Quick open file | | `Cmd/Ctrl + Shift + P` | Command palette | | `Cmd/Ctrl + F` | Find in file | | `Cmd/Ctrl + Shift + F` | Find in project | | `Cmd/Ctrl + D` | Add next match to selection | | `Cmd/Ctrl + /` | Toggle line comment | | `Cmd/Ctrl + B` | Toggle file tree | | `F2` | Rename symbol | | `F12` | Go to definition | | `Alt + Click` | Add multi-cursor | ## Terminal beside the editor Open the terminal panel for a real PTY attached to the running container. The terminal uses xterm.js (fit, weblinks, search addons) and connects over WebSocket. * **Docker mode** `docker exec` into the container * **Kubernetes mode** K8s API exec with PTY * **Desktop local** subprocess with PTY Shell sessions are persistent. An agent can `shell_open` a session, run a long-lived watcher, and you can attach to that same session from the terminal panel. ## Live preview The preview iframe sits alongside the editor. HMR flows into the iframe automatically. Save a file, the preview repaints. See [Live Preview](/guides/live-preview) for details. ## Design view integration Click into the Design view and the same editor stays in play. Clicking an element in the live preview jumps the editor to the JSX line that rendered it. Edits flow back through the same save API. See [Design Engineer](/guides/design-engineer). ## Empty directory handling The backend sends directories with no files as entries ending in `/` (empty content). `buildFileTree` in `CodeEditor.tsx` treats these as directories: ```ts theme={null} const isEmptyDir = file.file_path.endsWith('/'); const cleanPath = isEmptyDir ? file.file_path.slice(0, -1) : file.file_path; ``` Auto-select skips directory placeholders when picking the first file to open. ## Performance notes * Monaco loads lazily (the bundle is large); the editor shows a placeholder until it is ready * File tree is memoized; it only rebuilds when the files list changes * Autosave is debounced 1000ms by default ## Related The iframe next to the editor. Click-to-source canvas on top of the preview. The agent edits the same files you do. Diff, commit, push from the panel. # Communication Gateways Source: https://docs.tesslate.com/guides/communication-gateways Put an OpenSail agent into Telegram, Slack, Discord, WhatsApp, Signal, or a CLI WebSocket, and run agents on cron schedules with delivery to any channel. Tesslate OpenSail ## Overview OpenSail agents do not need to live inside the app. The **Gateway** is a persistent process that maintains live connections to messaging platforms so users can talk to an agent wherever they already work. A message from Telegram, Slack, Discord, WhatsApp, Signal, or an authenticated CLI WebSocket flows through the same pipeline as a browser chat, runs inside a sandboxed agent worker, and replies back to the originating thread, DM, or channel. Telegram, Slack, Discord, WhatsApp, Signal, and CLI WebSocket, each with inbound and outbound support. Agents run on their own with natural-language or cron expressions and deliver to any channel. 8-character pairing codes link platform accounts to OpenSail users via `PlatformIdentity`. ### Platform adapters | Platform | Inbound mode | Adapter file | | -------- | ----------------------------------------------------------- | ----------------------------------------------------- | | Telegram | Webhook or long-poll | `orchestrator/app/services/channels/telegram.py` | | Slack | Events API webhook or Socket Mode | `orchestrator/app/services/channels/slack.py` | | Discord | Gateway WebSocket via `discord.py`, or interactions webhook | `orchestrator/app/services/channels/discord_bot.py` | | WhatsApp | Meta Cloud API webhook | `orchestrator/app/services/channels/whatsapp.py` | | Signal | `signal-cli` REST SSE | `orchestrator/app/services/channels/signal.py` | | CLI | Authenticated WebSocket | `orchestrator/app/services/channels/cli_websocket.py` | ## Prerequisites Bring up the backend in Docker, Kubernetes, or desktop mode. See the [Self-Hosting quickstart](/self-hosting/quickstart) if you need the full install. Generate a base64 Fernet key: ```bash theme={null} python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ``` Set it as `CHANNEL_ENCRYPTION_KEY`. OpenSail falls back to `DEPLOYMENT_ENCRYPTION_KEY` and then `SECRET_KEY` (see `get_channel_encryption_key` in `orchestrator/app/config.py`), but an explicit channel key is strongly recommended. Set `REDIS_URL` if you want scheduled delivery, hot reload, or multi-pod routing. Without Redis the gateway still handles inbound messages in-process on desktop, but delivery to non-browser channels is disabled. `GATEWAY_ENABLED=true` is the default. The scheduler polls `agent_schedules` every `GATEWAY_TICK_INTERVAL` seconds (default 60). Telegram webhooks, Slack Events API, Discord interactions, and WhatsApp all require a public HTTPS endpoint. Socket-mode flavors (Slack Socket Mode, Discord gateway, Telegram long-poll, Signal SSE) work behind NAT with no inbound port. ## Architecture ``` Platform (Slack, Telegram, ...) | v webhook POST or outbound WS / poll /api/channels/webhook/{type}/{config_id} <--- HTTP ingress | | v v ChannelConfig (Fernet-encrypted creds) GatewayRunner (runner.py) | | | inbound MessageEvent | +---------------------------------------->+ | v TaskQueue (ARQ cloud / asyncio desktop) | v Agent worker runs agent loop | v Redis XADD -> gateway_delivery_stream | v GatewayRunner._delivery_consumer | v adapter.send_gateway_response(chat_id, text) | v Reply lands in the original chat ``` Key invariants: * One adapter per `ChannelConfig` row. Adapters are keyed by `config_id` inside `GatewayRunner.adapters`. * Per-session ordering: the runner derives a `session_key` from `platform:chat_type:chat_id[:thread_id]` so concurrent messages in the same chat are serialized. * Response routing uses Redis `XREADGROUP` on `gateway_delivery_stream`, so only the pod holding the session's adapter delivers the reply. * A `PlatformIdentity` row links a platform account (`telegram:12345`) to an OpenSail `User`, gating who the agent will respond to. ## Identity pairing Every platform user must be linked to an OpenSail user before the agent talks back. The user sends a message from the platform. The gateway sees no verified `PlatformIdentity` for the `(platform, platform_user_id)` pair. The gateway generates an 8-character code, stores it on an unverified `PlatformIdentity` row with `pairing_expires_at = now + GATEWAY_PAIRING_CODE_TTL` (default 1 hour), and sends it back through the same channel. The user opens OpenSail, navigates to Settings, and submits the code to `POST /api/gateway/pair/verify` with `{ "platform": "...", "pairing_code": "..." }`. See `orchestrator/app/routers/gateway.py`. The backend assigns `user_id`, sets `is_verified=true`, and stamps `paired_at`. All future inbound messages from that platform account run under that OpenSail user. Manage identities at `GET /api/gateway/identities` (list) and `DELETE /api/gateway/identities/{id}` (unlink). Rate limits flow through `GATEWAY_PAIRING_MAX_PENDING` and `GATEWAY_PAIRING_RATE_LIMIT_MINUTES`. ## Per-platform setup Configure all platforms from Settings > Channels in the OpenSail UI (`app/src/pages/settings/ChannelsSettings.tsx`) or directly through `POST /api/channels`. Credentials are Fernet-encrypted in `channel_configs.credentials`. Go to `https://api.slack.com/apps` and install it to your workspace. Under OAuth and Permissions, add: `chat:write`, `im:history`, `channels:history`, `groups:history`, `mpim:history`, `users:read`. Add `files:read` if you want attachments. Copy the bot token (`xoxb-...`) and the signing secret. * **Socket Mode (recommended)**: enable Socket Mode, generate an app-level token (`xapp-...`), and subscribe to `message.channels`, `message.im`, `message.groups`, `message.mpim`. No public URL required. * **Events API webhook**: paste `https:///api/channels/webhook/slack/` as the Request URL after creating the config. Settings > Channels > New > Slack. Paste `bot_token`, `signing_secret`, and optional `app_token`. Save. Invite the bot into any channel you want it to hear from. DMs work automatically. Open `@BotFather`, run `/newbot`, follow the prompts, copy the token. Settings > Channels > New > Telegram. Paste the token into `bot_token`. Save. The adapter defaults to long-polling via `getUpdates`, so no public URL is required. If you prefer webhooks, the adapter calls `setWebhook` with the generated URL and a secret token header. Paste additional tokens for agent swarm mode (multiple bot identities posting into the same chat). See `pool_tokens` in `telegram.py`. Go to `https://discord.com/developers/applications`. Under Bot, create a bot and copy the token. Under General Information, copy the Application ID and Public Key. Toggle `MESSAGE CONTENT INTENT` under Bot > Privileged Gateway Intents. Generate an invite URL with the `bot` and `applications.commands` scopes, plus `Send Messages`, `Read Message History`, `Use Slash Commands` permissions. Invite it to your server. Settings > Channels > New > Discord. Paste `bot_token`, `application_id`, and `public_key`. Save. The adapter defaults to a `discord.py` gateway WebSocket. For interactions-only mode, configure `https:///api/channels/webhook/discord/` as your Interactions Endpoint URL. Visit `https://developers.facebook.com` and create a WhatsApp Business app. Register the number and note the `phone_number_id`. Generate a permanent System User access token. Settings > Channels > New > WhatsApp. Paste `access_token`, `phone_number_id`, a random `verify_token`, and the `app_secret` from App Settings > Basic. In the Meta App Dashboard, under WhatsApp > Configuration, set Callback URL to `https:///api/channels/webhook/whatsapp/` and Verify Token to your value. Subscribe to `messages` events. WhatsApp is webhook-only. `supports_gateway` is False on this adapter. Signal requires a self-hosted `signal-cli` REST API (for example `bbernhard/signal-cli-rest-api` in JSON-RPC mode). Once your number is registered: Settings > Channels > New > Signal. Fill `signal_cli_url` (for example `http://signal-cli:8080`) and the registered `phone_number` in E.164 format. The adapter opens an SSE stream on `/v1/receive/{phone}` and posts outbound messages to `/v2/send`. Do not expose `signal-cli` publicly. Keep it on an internal network. The CLI adapter backs the Tesslate TUI and external API clients. No platform credentials are needed. A client connects to the gateway's CLI WebSocket route with a `tsk_` API key, OpenSail authenticates the key, and messages flow through the same `MessageEvent` pipeline. See `orchestrator/app/services/channels/cli_websocket.py` and the [agent docs](https://github.com/TesslateAI/tesslate-agent). ## Creating schedules Schedules let an agent run on a clock and push the result anywhere. The UI lives at `app/src/pages/settings/SchedulesSettings.tsx`. The API is `/api/schedules` (see `orchestrator/app/routers/schedules.py`). ```http Create a schedule theme={null} POST /api/schedules { "project_id": "...", "agent_id": "...", "name": "Friday summary", "schedule": "every Friday at 9am", "timezone": "America/New_York", "repeat": null, "prompt_template": "Summarize this week and post the highlights.", "deliver": "telegram:123456789" } ``` ```text Natural language theme={null} every 30m daily at 9:30am weekdays at 8am every monday at 6am nightly at 2am ``` ```text Cron theme={null} 0 9 * * 1-5 */15 * * * * 0 0 1 * * ``` Parser and runner files: * Natural language parser: `orchestrator/app/services/gateway/schedule_parser.py` * Any valid 5-field cron passes through unchanged. * `CronScheduler` (`orchestrator/app/services/gateway/scheduler.py`) ticks every `GATEWAY_TICK_INTERVAL`, picks due schedules, enqueues the agent task, increments `runs_completed`, and updates `next_run_at`. ### Delivery targets | Target | Meaning | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `origin` | Replies to whatever channel or chat created the schedule, using `origin_platform`, `origin_chat_id`, `origin_config_id` | | `telegram:` | Specific Telegram chat | | `discord:` | Specific Discord channel | | `slack:` | Specific Slack channel | | `whatsapp:` | Specific WhatsApp recipient | | `signal:` | Specific Signal recipient | Per-user limits are enforced by `GATEWAY_MAX_SCHEDULES_PER_USER` (default 50). ## Agents sending messages Agents can push messages into channels on their own using the `send_message` tool. Full reference: [tesslate-agent DOCS](https://github.com/TesslateAI/tesslate-agent). ```python theme={null} send_message( channel="discord", target="", text="Hello from the agent", ) ``` * `send_message` is a dangerous tool and is subject to plan-mode and approval gating (see `DANGEROUS_TOOLS` in the agent docs). * The backend resolves the right `ChannelConfig` for the current user and calls its adapter. Targets for DMs are the platform user ID; for channels, the channel or chat ID. * For quick Discord notifications without a full bot, set `AGENT_DISCORD_WEBHOOK_URL` and the tool will post through that webhook even if no Discord `ChannelConfig` exists. ## Credential security * Credentials are serialized to JSON and Fernet-encrypted before insert. See `encrypt_credentials` in `orchestrator/app/services/channels/registry.py`. * `CHANNEL_ENCRYPTION_KEY` must be a base64 Fernet key. If a raw string is provided, the registry derives a Fernet key through SHA-256 to avoid a cold-start crash, but this path is a fallback, not a recommendation. * `GET` endpoints on `/api/channels` never return raw credentials. The UI masks values via `_mask_credentials` in `orchestrator/app/routers/channels.py`. * Webhook URLs include a per-config `webhook_secret` (see `generate_webhook_secret`) on top of platform signature verification: HMAC-SHA256 for Slack and WhatsApp, Ed25519 for Discord, secret-token header for Telegram. * Key rotation: generate a new key, re-encrypt existing rows, set the new key. Channel credentials are small, so a migration script that reads decrypted values and writes them back with the new key works cleanly. ## Voice transcription Set `GATEWAY_VOICE_TRANSCRIPTION=true` (default) and `GATEWAY_VOICE_MODEL` (default `whisper-1`) to transcribe inbound voice messages before they reach the agent. The gateway downloads audio via the platform's file API (for example Telegram `getFile`, Slack `files.info`), sends it to the configured LiteLLM transcription model, and forwards the resulting text as a normal `TEXT` `MessageEvent`. Cached media lives under `GATEWAY_MEDIA_CACHE_DIR` and ages out after `GATEWAY_MEDIA_CACHE_MAX_AGE_HOURS`. ## Testing and debugging Useful places to look when something is off: * `kubectl logs deploy/tesslate-gateway --context=` or `docker compose logs -f gateway`. Look for `[GATEWAY]`, `[TG-GW]`, `[SLACK-GW]`, `[DISCORD-GW]`, `[SIGNAL-GW]` prefixes. * `GET /api/gateway/status` returns shard count, adapter count, heartbeat, last sync timestamp. * `ChannelMessage` rows log inbound and outbound messages with `direction`, `status`, `platform_message_id`, and the associated `task_id`. Query by `channel_config_id`. * `POST /api/gateway/reload` forces a diff-based adapter resync. ### Common failure modes | Symptom | Likely cause | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | Slack webhook returns 401 | Wrong `signing_secret`, or clock skew greater than 300 seconds. The gateway rejects any timestamp outside that window. | | Discord signature failures | Wrong `public_key`, or the body was rewritten by a reverse proxy. Verify Ed25519 over raw bytes. | | Telegram sends messages but never receives | `setWebhook` was called but the URL is unreachable. Switch to long-poll mode by leaving the webhook unset. | | WhatsApp webhook never fires | `verify_token` mismatch, or the app is not subscribed to `messages` events. | | Bot ignores a message | No verified `PlatformIdentity` for that sender. The gateway should have replied with an 8-character pairing code. | | Schedule fires but no delivery | Redis is not configured, so the delivery consumer is a no-op. Set `REDIS_URL`. | ## Production notes * `GATEWAY_SHARD` and `GATEWAY_NUM_SHARDS` shard `ChannelConfig` rows across gateway replicas. Each config is pinned to a shard via `channel_configs.gateway_shard`. * Use a `Recreate` update strategy with `replicas=1` per shard so there is only ever one active adapter per config. The file lock at `GATEWAY_LOCK_DIR` is defense in depth for Docker Compose. * `GATEWAY_SESSION_IDLE_MINUTES` controls how long an inactive session holds its place in the runner's per-session ordering (default 1440). * `GATEWAY_TICK_INTERVAL` is the cron scheduler granularity. Sixty seconds is a good default. Dropping below 30 raises load without gaining much resolution. * Heartbeat keys `tesslate:gateway:active:{config_id}` expire every 120 seconds. Use them in Grafana for liveness. * The `gateway_delivery_stream` Redis Stream is capped at `GATEWAY_DELIVERY_MAXLEN` entries (default 10000) so crashes never lose pending deliveries forever. ## Where to next Ship your agent's containers to 22 external providers in the same canvas flow. Package an agent workspace as an installable Tesslate App with its own billing. Full surface for `send_message`, approval policy, plan mode, and tool gating. # Connectors and MCP Source: https://docs.tesslate.com/guides/connectors-mcp Wire Slack, Gmail, Linear, GitHub, and dozens of other tools into any OpenSail agent using the Model Context Protocol Tesslate OpenSail ## What MCP is Model Context Protocol (MCP) is an open protocol for exposing tools, resources, and prompts from an external server to an LLM agent. OpenSail uses MCP to wire real third-party systems (Slack, Gmail, Google Drive, Linear, GitHub, Brave Search, Postgres, filesystems, and more) into any agent's tool registry. From the agent's perspective, MCP tools look identical to built-in tools. The agent calls `slack__post_message` or `linear__create_issue` exactly like it calls `write_file`. The protocol is open, so nothing in your stack is locked to Tesslate. Slack, Gmail, Drive, Linear, GitHub, Brave, Postgres, filesystem, and more You install a connector with your credentials, encrypted at rest with Fernet Attach a connector to a specific agent or reuse across many Standard MCP server; BYO server for internal tools ## Architecture OpenSail's MCP layer has three pieces, all on the orchestrator side. `orchestrator/app/services/mcp/client.py` opens a streamable-HTTP connection to an MCP server, initializes the session, and lists its tools, resources, and prompts. Streamable HTTP is the only supported transport (see "Why not stdio" below). `orchestrator/app/services/mcp/bridge.py` turns MCP capabilities into native Tesslate `Tool` objects. Each tool is name-prefixed with the server slug (`github__create_issue`, `slack__post_message`) and gets a stateless executor that reconnects to the server on each call. `orchestrator/app/services/mcp/manager.py` is the per-user orchestrator. It resolves which servers are attached to which agent, loads encrypted credentials, connects to each server, caches tool schemas in Redis, and bridges the results into the agent's `ToolRegistry` right before the session starts. ### Why streamable HTTP only MCP defines two transports: stdio (child process per user per call) and streamable HTTP (stateless HTTP). OpenSail supports only streamable HTTP. Stdio was designed for single-user desktop apps. In a multi-tenant environment it requires Node.js on every orchestrator pod, spawns a process per call per user, and doesn't scale horizontally. Streamable HTTP offloads load to the connector provider, works identically across replicas, and adds zero dependencies to the backend container. When adding a new MCP server to the seed catalog, verify the server publishes a streamable-HTTP endpoint. If it's stdio-only, OpenSail cannot use it until the maintainer ships an HTTP transport. ## The user journey Open the Marketplace and filter to MCP Servers. Each card shows the server name, the tools it exposes, the auth type (none, bearer), and any required credential fields. Click Install. The connector lands in your Library as a `UserMcpConfig`. If the server requires credentials, you are prompted to enter them. Credentials are encrypted at rest with Fernet before being stored. From the Library MCP card, click Test Connection. OpenSail opens a streamable-HTTP session to the server, lists capabilities, and returns tool/resource/prompt counts. From the Library MCP card, click Add to Agent and pick from your agent list. This creates an `AgentMcpAssignment` row linking the connector to the agent. Start a chat with that agent. The worker bridges the connector's tools into the agent's registry before the first model call. The session header shows a purple badge for each attached MCP server. Per-user limits apply: the `mcp_max_servers_per_user` setting (default 20) caps how many connectors any one user can install. Enough for most workflows, prevents registry bloat. ## Real connectors OpenSail's marketplace ships or is adding official support for: Post messages, read channels, manage threads Read, send, label, draft, thread operations List, search, read, write, and share files Create and update issues, comments, projects, cycles Issues, PRs, files, commits, releases Web search with privacy-first provider Scoped filesystem access for local workflows Read-only or read-write SQL access Library documentation and code examples More connectors ship on the marketplace regularly. Any streamable-HTTP MCP server works: if your internal system has an MCP server (or can wrap one), you can install it. ## Managing connectors Every connector is stored as a `UserMcpConfig` row for your user. Key operations: | Operation | Where | | ------------------------ | ----------------------------------------- | | Install | Marketplace detail page, click Install | | Update credentials | Library MCP card, Credentials button | | Test connection | Library MCP card, Test button | | Re-discover capabilities | Library MCP card, Details panel, Refresh | | Attach to agent | Library MCP card, Add to Agent dropdown | | Detach from agent | Agent detail page, MCP tab, remove | | Uninstall | Library MCP card, Uninstall (soft-delete) | Tool schemas are cached in Redis for `mcp_tool_cache_ttl` seconds (default 300). When you rotate credentials or change enabled capabilities, the cache is automatically invalidated. ## Attaching to an agent Open the agent detail page from your Library. Go to the MCP Servers tab. Pick from your installed connectors. Attachment is idempotent; re-adding a previously disabled assignment re-enables it. Open a chat with that agent. The session header shows a purple plug badge for each attached MCP. The tool list in the agent's first turn includes prefixed tool names like `slack__post_message`. Tools can pile up and dilute the agent's focus. If a connector isn't being used, detach it. You can always re-attach later. ## Authoring your own connector For internal tools, you can run your own MCP server and install it on OpenSail exactly like a marketplace connector. Use the official MCP SDK (Python, TypeScript, or another language) and expose a streamable-HTTP endpoint. Stdio transport is not supported. Supported auth: `none` or `bearer`. If bearer, your clients pass a token via the Authorization header. Run it anywhere reachable over HTTPS. You can run it inside an OpenSail project if you want it co-located. Contact Tesslate to add it to the marketplace, or install it directly via API with a custom `UserMcpConfig`. ## OpenSail as an MCP server OpenSail also exposes itself as an MCP server. External coding agents (Claude Code, Cursor, Codex, or your own) can connect to your OpenSail instance and get sandboxed compute, invoke your agents, and manipulate projects through a standard MCP interface. See the Gateway API section of the platform docs for details. ## Security All MCP credentials are encrypted at rest using Fernet symmetric encryption. The encryption key is held server-side in `CHANNEL_ENCRYPTION_KEY`. Credentials are decrypted only at call time and never logged. Every MCP install and attachment is scoped to your user. Other users (even on the same team) never see your credentials or your attachments. MCP tools participate in the standard edit-mode gating. If you're in Ask Before Edit, dangerous MCP tools still prompt for approval before execution. Rotate credentials in the Library. Uninstall to soft-delete the config and invalidate the schema cache. Tokens can be revoked at the provider at any time; OpenSail will fail closed on the next call. ## Troubleshooting Check that the `AgentMcpAssignment` exists and is enabled. Verify the chat is using the expected agent (worker logs show `MCP context query: agent_id=...`). Re-run Test Connection from the Library to force a fresh discovery. The MCP server is unreachable. Check the configured URL, the server's uptime, and any network restrictions. Some servers rate-limit by user key; verify your credential is still valid. This error means the connector's config specified stdio. Only streamable HTTP is supported. If the connector's maintainer doesn't ship an HTTP endpoint, OpenSail cannot install it. Default MCP tool timeout is 30 seconds (`mcp_tool_timeout`). Some tools (large Drive searches, complex Postgres queries) need longer. The setting is orchestrator-wide; contact your admin if you're self-hosting. Schemas are cached in Redis for 5 minutes by default. If a connector added new tools and the agent isn't seeing them, run Re-discover from the Library card. This invalidates the cache and fetches fresh schemas. ## Next steps Bind connectors to custom agents Pair connectors with skill playbooks for end-to-end workflows Manage external API keys for model BYOK and agent invocation See MCP tool calls in the chat transcript # Creating Projects Source: https://docs.tesslate.com/guides/creating-projects Three paths to a new OpenSail project: pick a template, import a git repo, or describe what you want and let the agent scaffold it. Tesslate OpenSail ## Three ways to start OpenSail gives you three entry points. Pick the one that matches how you are starting. Marketplace base with working code, containers, and config. GitHub, GitLab, or Bitbucket repo, including private with OAuth. Natural language. The agent scaffolds the project for you. All three run through the same `POST /api/projects` endpoint. A background task sets up the directory, seeds the workspace, and the frontend polls `GET /api/tasks/{task_id}` for progress. When the task completes, you land in the project builder. ## Method 1: From a template The marketplace ships dozens of bases covering every popular framework. Pick one and you get working code, pre-wired containers, and a valid `.tesslate/config.json`. Navigate to `/dashboard` and click **Create Project** or use the create button on `/home`. The wizard shows the featured bases. Browse all of them from the marketplace browse route. Common picks: * **Next.js 16** for full-stack React with App Router * **Vite + FastAPI** for React frontend + Python API * **Expo** for React Native mobile * **Go API** for Go backends * **Full-stack CRM** for multi-container starters Pick a project name. The slug auto-generates (`my-app-k3x8n2`). Create submits the request and the background task begins. The task clones the base repo, seeds the volume, and registers containers. Progress: 5% directory created, 10-90% clone and file sync, 100% done. The frontend navigates to `/project/{slug}/builder` when setup completes. Containers are stopped; click Start to boot them. Templates are just `MarketplaceAgent` rows with `item_type='base'` and a `git_repo_url`. Publishing your own base is covered in the marketplace docs. ## Method 2: Import from git OpenSail supports GitHub, GitLab, and Bitbucket, including private repos via OAuth. The first import prompts OAuth with your git provider. Tokens are stored encrypted in `DeploymentCredential` and reused for future imports, push, pull, and commit. The import modal lists your accessible repos (pulled from the provider's API). Filter by owner, search by name. Private repos are listed alongside public ones. Default branch is pre-selected. Override for feature branches or forks. Submit. The background task clones the repo, seeds the workspace, and runs the Librarian agent against the tree to generate `.tesslate/config.json`. The setup wizard opens at `/project/{slug}/setup`. The Agent tab shows the LLM-generated graph; the Manual tab lets you hand-edit the same `ServiceConfigForm`. Confirm to save. Deep-link imports work too. An external "Edit in OpenSail" button can link to `/import?repo=https://github.com/user/repo`. The auth-aware redirect sends signed-in users to the import flow and unauthenticated users to login while preserving the destination. ### Supported providers | Provider | OAuth | Private repos | Push/pull | | ------------- | ----- | ------------- | --------- | | **GitHub** | Yes | Yes | Yes | | **GitLab** | Yes | Yes | Yes | | **Bitbucket** | Yes | Yes | Yes | ## Method 3: Describe what you want The "describe" flow gives the agent a blank workspace and your prompt. The agent picks an appropriate base, adjusts it, and iterates until the scaffold matches your description. From the dashboard, click **Describe your app** (or use the command palette). Be specific. "A Slack-style team chat app with channels, DMs, and search, built with Next.js and Postgres" gives the agent enough to scaffold. "An app" does not. If you are on Kubernetes, choose the compute tier. Tier 0 scaffolds without a compute pod; tier 1 or 2 boot containers so the preview works immediately. The agent picks a base, runs `apply_setup_config` to wire containers, installs dependencies, and writes an initial implementation. You watch progress in the chat panel. When the agent stops, the preview is live. Ask for changes, review diffs, approve or reject. The describe flow uses the same agent loop as any chat session. Progressive persistence means you can close the tab, grab coffee, and come back to a finished scaffold. ## What happens under the hood All three methods produce the same end state: a `Project` row, a provisioned volume, container records, and a valid `.tesslate/config.json`. Creates the Project row with status `creating`, assigns a unique slug, kicks off the background task, returns the project and a `task_id`. Runs in the background. Creates the directory (Docker) or allocates the volume (K8s). Clones the source (base or git). Writes files to the volume. Reads `.tesslate/config.json` from disk. If absent, falls back to LLM analysis (Librarian agent), then to a minimal single-app fallback. Upserts Container rows from the config. Creates ContainerConnection, DeploymentTarget, and BrowserPreview records as declared. The frontend poll flips to 100%. Navigation to `/project/{slug}/builder`. Containers are stopped and ready to start. ## Compute tier selection On Kubernetes projects, you can pick a compute tier at creation time: No compute provisioning. The workspace exists, but nothing runs. Use when you just want source code and will run locally. Tier 1 warm ephemeral pool. Shell and quick operations work; long-running dev servers do not. Cheap, fast. Full Tier 2 namespace with live preview, persistent dev servers, and hibernation. What you want for active development. You can upgrade or downgrade later from the Project Overview page. ## After creation Once the project is ready, the usual surfaces are available: Monaco with the project file tree. Iframe with hot module reload. Visual graph editor for the config. Talk to agents about your project. # Customizing Agents Source: https://docs.tesslate.com/guides/customizing-agents Create a custom agent with a system prompt, bound skills, bound MCP connectors, a preferred model, and an approval policy Tesslate OpenSail ## Why customize An agent in OpenSail is a `tesslate-agent` instance plus a system prompt, a bound skill catalog, a bound MCP connector set, a model preference, and an approval policy. Customization is how you turn a generic coding agent into a specialist: a Tailwind UI builder, a FastAPI API author, a security reviewer, a product-ops bot that lives in Slack. Lock in coding conventions, frameworks, and patterns once, reuse forever Restrict tools, skills, and MCP connectors so the agent does one thing well Publish to your team or the public marketplace as an immutable agent version Approval policies travel with the agent, so dangerous actions still gate the same way ## The five levers The character sheet for the agent: role, tone, rules, tech stack, and anti-patterns. System prompts support runtime markers like `{mode_instructions}`, `{project_name}`, and `{git_branch}` that are substituted before each model call. Skills are reusable markdown capabilities (a code-review checklist, a research methodology, a report template). You attach skills to an agent via the Library. The agent sees the catalog at session start and loads full bodies on demand. See `/guides/skills`. Model Context Protocol connectors expose third-party tools (Slack, Gmail, Linear, GitHub) to the agent. You install a connector on your account, attach it to the agent, and its tools get bridged into the agent's registry at runtime. See `/guides/connectors-mcp`. The LiteLLM-routed model used for each session. Pick Anthropic, OpenAI, DeepSeek, Qwen, Google, or any other supported provider. Users can override at runtime if the agent allows it. See `/guides/model-management`. The default edit mode (Ask Before Edit, Allow All Edits, or Plan Mode) baked into the agent. Mode is enforced at the tool registry so it applies uniformly regardless of the system prompt text. ## Creating an agent ### Fork an existing one The fastest path. You start with a working system prompt and adjust. Open the Marketplace or the Library and find an agent close to what you want. Click Fork. A copy lands in your Library with your ownership. Rewrite the system prompt to match your conventions. Keep `{mode_instructions}` somewhere in the prompt so edit-mode behavior stays consistent. Go to the agent's detail page. Add skills from the Library tab. Add MCP connectors from the MCP tab. Set a default model. Choose something faster for quick iteration, something stronger for reasoning-heavy tasks. Open a test project, run the agent against a representative task, and iterate on the prompt. ### Create from scratch Library then the Agents tab. Click Create New Agent. Give it a slug, a display name, an icon, a category, and a short description. These power marketplace search. Write the prompt. Keep it focused: one role, one stack, one set of rules. Attach the skills this agent needs. Leave off anything speculative: skills are lazy-loaded and only cost context window on use. Attach the connectors this agent must have. Connectors are per-user, so installers attach their own credentials after install. Set the default model and the default approval policy. Save. Open a project and run through real tasks. ## Writing system prompts A good system prompt has five sections: role, guidelines, stack, constraints, runtime markers. One line. Who the agent is and what it does. ``` You are an expert TypeScript and React developer. You build accessible, responsive components using Tailwind and React Hook Form. ``` How the agent should work. ``` Always: - Read the target file before editing - Use named exports - Type every prop and return value - Follow the patterns already in the project ``` Preferred libraries. ``` Stack: - React 18+ with TypeScript strict mode - Tailwind CSS, no CSS-in-JS - React Hook Form + Zod - React Query for server state ``` What to avoid. ``` Avoid: - Class components - Default exports - Hardcoded colors outside the Tailwind palette - Monolithic components; split above 200 lines ``` Dynamic context injected at each call. ``` {mode_instructions} Project: {project_name} Branch: {git_branch} Available tools: {tool_list} ``` ### Common markers | Marker | Substituted with | | ----------------------- | ------------------------------------------------------ | | `{mode_instructions}` | Full text for the current edit mode (allow, ask, plan) | | `{mode}` | The mode name only | | `{project_name}` | Current project display name | | `{project_description}` | Project description | | `{project_path}` | Workspace path inside the container | | `{git_branch}` | Current branch | | `{tool_list}` | Comma-separated tool names available this session | | `{user_name}` | The signed-in user's display name | | `{timestamp}` | ISO timestamp at call time | Always keep `{mode_instructions}` in your prompt. It guarantees the agent still honors Plan Mode and Ask Before Edit even if your other instructions conflict. ## Binding skills Skills are the unit of portable know-how in OpenSail. Each skill is a markdown document with a name, a short description, and a body. The agent only sees the name and description at session start. When it decides it needs one, it calls `load_skill` and the full body is injected into the turn. Three sources feed the catalog: 1. **Built-in skills** shipped with the platform (for example, `project-architecture`). Available to every agent. 2. **Skills you installed on this agent** via the Library. You attach them explicitly. 3. **Project-file skills** discovered at `.agents/skills/SKILL.md` in the workspace. Any agent working in that project sees them. For authoring skills, see `/guides/skills`. ## Binding MCP connectors MCP (Model Context Protocol) is how OpenSail bridges third-party tool servers into the agent's tool registry. You install a connector once on your account (with your credentials, encrypted at rest), then attach it to any agent. When the agent starts a session, the worker: 1. Looks up every `AgentMcpAssignment` for the agent 2. Loads the user's `UserMcpConfig` for each (with credentials) 3. Connects to each MCP server over streamable HTTP 4. Bridges the server's tools, resources, and prompts into the agent's `ToolRegistry` From the agent's perspective, connector tools look identical to built-in tools. They get used the same way. For connector setup and credential management, see `/guides/connectors-mcp`. ## Approval policy Every agent carries a default edit mode. Users can override per session, but your default matters because it sets the expected behavior. | Default | When to pick it | | --------------- | -------------------------------------------------------------------------------------------------------- | | Ask Before Edit | Any agent that writes files or runs shell commands. Safest default. | | Allow All Edits | Agents meant for trusted contexts (personal projects, sandboxed environments). | | Plan Mode | Agents that should only plan and never execute. Useful for review agents, security scanners, architects. | ## Publishing to the marketplace Clear system prompt, representative test runs, working skill and MCP bindings. Fill in the marketplace description, category, and tags. Add example prompts that show the agent at its best. Private (just you), team-only, or public. Public listings go through the approval pipeline. Free, one-time purchase, subscription, or API-metered. Creator payouts pay 90% to you, 10% to the platform. See `/guides/billing`. Submit for review. The approval pipeline runs automated security and manifest checks, then a sandbox evaluation, then a human review. Publish updates as new immutable versions. Installers can pin, auto-update, or manually update per their policy. Public marketplace agents are visible to everyone. Do not hardcode secrets or environment-specific paths into system prompts. Use runtime markers for project-specific context. ## The MarketplaceAgent model Agents are stored as `MarketplaceAgent` rows with `item_type="agent"`. The relevant columns for customization: | Field | Purpose | | ------------------------------------------------- | ---------------------------------------------------------------- | | `slug` | Stable identifier | | `name`, `description`, `icon`, `category`, `tags` | Marketplace metadata | | `system_prompt` | The prompt body (markdown with runtime markers) | | `tools` | Optional allowlist of tool names; null means all available tools | | `model` | Default LiteLLM model id | | `approval_mode` | Default edit mode | | `visibility` | Private, team, or public | | `price` | Free, one-time, subscription, or API-metered | | `forkable` | Whether installers can fork the agent | | `is_builtin` | Reserved for platform-shipped agents; not user-writable | Attachments live in adjacent tables: * `AgentSkillAssignment` binds a skill to an agent * `AgentMcpAssignment` binds an installed MCP server to an agent ## Testing your agent Make a dedicated workspace. Clone a typical project you'd use this agent on. Not just "hello world". Run the actual tasks users will throw at this agent. Put the agent in Ask Before Edit. Review what it wants to do. Note where it picks the wrong tool or the wrong file. Does it follow your guidelines or ignore them under pressure? Add explicit rules for the failure modes. Long-horizon tasks reveal whether your prompt holds up. Run multi-step workflows and watch for off-pattern code. ## Best practices Focused agents outperform generalists. "React + Tailwind UI specialist" beats "full-stack developer" on UI tasks and vice versa. Every time the agent does something wrong, add a rule that prevents it. Every time it does something right, generalize it into the prompt. Prompts are versioned like code. A review agent does not need `bash_exec`. A docs writer does not need `multi_edit`. Narrow tools reduce drift and lower risk. LLMs follow examples better than abstractions. Include a short code snippet in your system prompt showing the pattern you want. Ship new agents in Ask Before Edit. Let users opt into Allow All once they trust the behavior. ## Next steps The user-side guide to running agents in chat Author and publish reusable agent skills Wire external tools into any agent Choose models, BYOK, or self-host # Deployment Targets Source: https://docs.tesslate.com/guides/deployment-targets Ship containers to 22 external providers from the Architecture Panel with a single edge connection, per-target environments, and independent rollback. 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. Serverless, static, container push, and registry export providers, all wired into the same canvas flow. Drop a second target, point production at Vercel, preview at Cloudflare Pages, staging at Fly.io. Cycle `production`, `staging`, `preview` on the target header. Env-var merge runs in four layers. 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) | Provider | Slug | Auth | Notes | | ---------------------------------- | -------------- | --------- | ---------------------------------------- | | Vercel | `vercel` | OAuth | Source mode default, framework detection | | Netlify | `netlify` | OAuth | Pre-built upload via Netlify Files API | | Cloudflare Pages and Workers | `cloudflare` | API token | Pre-built, `account_id` required | | DigitalOcean App Platform (source) | `digitalocean` | API token | Requires git remote on project | | Railway | `railway` | API token | Requires git remote | | Fly.io | `fly` | API token | Container push to Fly Machines | | Heroku | `heroku` | API key | Source tarball upload | | Render | `render` | API key | Requires git remote | | Koyeb | `koyeb` | API token | Serverless, optional `org_slug` | | Zeabur | `zeabur` | API key | ZIP upload, `region` optional | | Northflank | `northflank` | API token | Requires git remote | ### Static hosting (4) | Provider | Slug | Auth | Notes | | ---------------- | -------------- | -------------------- | ------------------------------------- | | GitHub Pages | `github-pages` | GitHub PAT | `repo` scope, static only | | Surge | `surge` | Email + token | Run `surge token` in the CLI to fetch | | Deno Deploy | `deno-deploy` | Access token | `org_id` required | | Firebase Hosting | `firebase` | Service-account JSON | `site_id` required | ### Container push (4) | Provider | Slug | Auth | Notes | | --------------------------- | ---------------------- | ------------------------------------ | ------------------------------ | | AWS App Runner | `aws-apprunner` | Access key + secret | Pushes to ECR, then App Runner | | GCP Cloud Run | `gcp-cloudrun` | Service-account JSON | Pushes to Artifact Registry | | Azure Container Apps | `azure-container-apps` | Tenant, client, secret, subscription | Pushes to ACR | | DigitalOcean Container Apps | `do-container` | API token | Pushes to DOCR | ### Registry and export (3) | Provider | Slug | Auth | Notes | | ------------------------- | ----------- | --------------------------------- | ------------------------------------ | | Docker Hub | `dockerhub` | Username + PAT | Registry push only, no runtime | | GitHub Container Registry | `ghcr` | Username + PAT (`write:packages`) | Registry push only | | Download and Export | `download` | None | Zip 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. Every other provider takes a long-lived API token or service-account JSON. Field labels and per-field help strings come from `app/src/lib/deployment-providers.ts` (`PROVIDER_CREDENTIAL_HELP`). The same strings render as hover tooltips on the canvas node while the target is still unconnected. Credentials are user-scoped by default. Pass `project_id` on the create request to pin a credential to a single project. The resolver in `get_credential_for_deployment` (`routers/deployments.py`) prefers the project-scoped credential over the user default. 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 Open your project, switch to the Architecture tab, and confirm your containers are visible. 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`. 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. Click the environment label (`production`, `staging`, `preview`) on the target header to cycle values. This writes back to `DeploymentTarget.environment`. 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`. 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](/guides/architecture-panel). ## Triggering a deploy Three entry points, one pipeline: `POST /api/projects/{slug}/deployment-targets/{target_id}/deploy`. Loops over every connected container in one request. This is the standard path. `POST /api/deployments/{project_slug}/deploy` with `provider`, `deployment_mode`, `env_vars`, and `custom_domain`. Powers the top-right Deploy button. The agent can deploy on your behalf. See the [agent tool reference](https://github.com/TesslateAI/tesslate-agent). ## 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. | Layer | Source | Typical use | | ------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------- | | 1. Auto-derived | `project.git_remote_url` becomes `_TESSLATE_REPO_URL` | Git-required providers | | 2. Target-level | `DeploymentTarget.deployment_env` JSON | Values shared by every container on the target (`NODE_ENV=production`) | | 3. Connection-level | `DeploymentTargetConnection.deployment_settings["env_vars"]` | Per-container differences against the same target | | 4. Request-level | `env_vars` passed in the deploy call | One-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 Every `access_token_encrypted` column is Fernet-encrypted with the backend key. Decryption happens only inside the deployment pipeline, never in the API response. 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). `GET /api/deployment-credentials` returns metadata and the `is_default` flag. The token field is never serialized back to the client. 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 Turn a deployed project into a distributable Tesslate App with versioning, approval, and billing. Wire a deployed backend to Telegram, Slack, Discord, or WhatsApp via the gateway. Learn the full canvas: containers, connections, env injection, and deployment edges. # Design Engineer Source: https://docs.tesslate.com/guides/design-engineer Click any element in the live preview and jump to the exact line of source code that rendered it. Two-way sync between the preview, the editor, and the agent. CSS inspector, Insert palette, visual-first iteration. Tesslate OpenSail ## What it is Design Engineer turns the Live Preview into a first-class editor. Click any element and the code that renders it opens, highlighted, in Monaco. Inspect the element's computed CSS, tweak props visually, insert new components from a palette, and let the agent keep track of what changed. No context-switching from browser to editor. Click any rendered element, land on the exact file and line that produced it. Changes in the editor update the preview. Edits on the preview update the source. Inspect any element's computed styles, classes, and Tailwind utilities. Drop pre-built sections, components, or primitives into the running app. ## How click-to-source works OpenSail's dev server injects a `data-oid` attribute on every JSX element it compiles. The attribute encodes the file path and the AST location. When you click an element in the preview iframe, the overlay reads `data-oid` and tells Monaco to jump to the exact line. ```html theme={null}
``` The OID index is kept in memory on the backend and rebuilt incrementally as the agent or you edit files. No custom build step on your end. The `data-oid` attribute is only added in dev mode. Production builds strip it automatically. Your shipped app never leaks source paths. ## The workflow Toggle the Design tab in the builder. The preview iframe gets a transparent selection overlay that tracks hover and click. Hover to highlight an element's bounding box. Click to select. The editor jumps to the source and highlights the matching JSX block. The CSS inspector panel opens beside the preview with computed styles, applied classes, parent chain, and bounding box metrics. Change a class, a prop, or the tree structure. Save. The preview hot-reloads with the change in place. Select an element and ask in chat: "make this card use a subtle gradient". The agent already knows which file and block you mean. ## The CSS inspector Every computed CSS property resolved at the browser layer. Useful for understanding cascade conflicts or unexpected values. The Tailwind utility classes currently applied, with their resolved values. Click any class to remove it. Click the plus button to add a new class with autocomplete. Bounding box, margin, padding, flex/grid parent info. Same model the browser devtools use, embedded in the studio. The JSX block that rendered the element, with the prop values it received. Jump to the component definition to see the source. ## Two-way sync Every edit source roundtrips both directions. | You do | What updates | | ------------------------------------ | ------------------------------------------------------- | | Save a file in Monaco | Preview hot-reloads via Vite HMR | | Click an element in the preview | Editor jumps to the source block | | Change a class in the inspector | File is patched and saved atomically | | Drag a block from the Insert palette | JSX is inserted at the chosen position; file saves | | Agent runs `apply_patch` | Preview hot-reloads; any selected element re-highlights | The inspector's class edits go through `patch_file` under the hood, so they produce clean diffs the agent can see and you can commit. Nothing is edited in memory only. ## The Insert palette The Insert palette is a grid of pre-built sections, components, and primitives keyed to your project's framework and design system. It ships with curated content for Vite React and Next.js projects and picks up custom entries from your project's `.tesslate/components.json`. Click Insert or press `I` with the preview focused. Browse by category (Hero, Pricing, Feature Grid, Nav, Footer, Form, Table). Every block is a live preview itself. Drag the block onto the preview. A drop indicator shows the insertion point. Release to insert. The block is now source code in your project. Edit props, swap content, restyle through the inspector or chat. ## Agent + Design Engineer together The visual layer is complementary to the agent. Typical patterns: Select an element and say "this is too cramped". The agent sees the element OID, reads the surrounding code, and suggests spacing changes with a preview diff. Select one card. Ask: "apply this pattern to every card on this page". The agent finds all matching elements and applies the update consistently. Select a block. Ask: "extract this into a reusable component". The agent creates the file, replaces the inline block with a call, and the preview stays identical. Drop a screenshot into chat. Say "rebuild the hero section to match this". The agent generates the JSX, you refine the spacing and colors visually. ## Frameworks supported Design Engineer is framework-aware: | Framework | OID injection | Insert palette | Inspector | | ---------------------------------- | ------------- | -------------- | --------- | | Vite + React (TSX/JSX) | Yes | Yes | Full | | Next.js (App Router, Pages Router) | Yes | Yes | Full | | Astro (`.astro` components) | Yes | Limited | Full | | SvelteKit | Roadmap | Roadmap | Basic | | Vue (Vite) | Roadmap | Roadmap | Basic | For frameworks without OID support yet, you still get the CSS inspector and HMR-backed preview; click-to-source falls back to DOM-path heuristics. ## Performance Click-to-source resolves in under 50 ms on normal page sizes. The overlay is a single absolutely-positioned React tree that only re-renders on hover or selection change. Classes applied via the inspector write through the same file-patch pipeline as the agent, so no special handling needed. Design mode runs only in dev. The OID attributes, overlay, and inspector are stripped from production builds automatically by the dev-server plugin. ## Keyboard | Shortcut | Action | | ------------------ | ------------------------------------ | | `I` | Open Insert palette | | `Esc` | Clear selection | | `Shift + click` | Select parent element | | `Alt + click` | Inspect without selecting | | `Cmd/Ctrl + click` | Jump to source, keep preview focused | ## Best practices Drop a hero or a pricing table, then customize. Faster than building from zero. Every inspector tweak is a real file edit. Review diffs and commit when you like what you see. Selection context gives the agent a head start. "Fix this" with an element selected beats "fix the hero section". TypeScript props light up better in the inspector. Use `interface` or `type` declarations on every component. ## Troubleshooting The dev server may not have injected OIDs yet. Hard-refresh the preview. If it persists, restart the dev container; the OID plugin picks up on Vite's next boot. Wait for the HMR patch to land. If the file was just edited and the inspector shows old values, click the element again to re-query the DOM. The palette indexes from your framework and from `.tesslate/components.json`. Make sure your project runtime is supported or the custom catalog file exists. ## Related The iframe the overlay runs on top of. Monaco's end of the click-to-source jump. Canvas-level structure; Design Engineer handles the rendered output. Pair selections with chat for visual-first development. # Docker Setup Source: https://docs.tesslate.com/guides/docker-setup Run Tesslate OpenSail locally with Docker Compose. Fresh clone to a working project in under 20 minutes. Tesslate OpenSail A complete walk-through for getting OpenSail running locally with Docker Compose on macOS, Windows (WSL 2), or Linux. By the end you will have a logged-in user, a working project, and a live preview at `http://localhost`. If you just need commands, jump to the [Quick Start](#quick-start). For the native desktop app, see the [Desktop install guide](/guides/desktop-install). ## 1. Prerequisites | Tool | Minimum | Notes | | ----------------------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------- | | [Docker Desktop](https://www.docker.com/products/docker-desktop/) | 4.30+ (Engine 26+, Compose v2) | Required on macOS, Windows, and Linux desktops. Enable the WSL 2 backend on Windows. | | `docker compose` | v2.27+ | Ships with Docker Desktop. This guide uses the `docker compose` (no hyphen) form. | | `git` | 2.40+ | For cloning the repo. | | Disk | 15 GB free | Images are about 2.5 GB. Named volumes (Postgres, projects, base cache, Redis) grow with use. | | RAM | 8 GB minimum, 16 GB recommended | Orchestrator, worker, gateway, and user containers add up. | | CPU | 4 cores | Vite HMR and agent runs are CPU sensitive. | Node.js and Python are NOT required on the host. Everything runs inside containers. ### OS support macOS 13+ on Intel or Apple Silicon. Docker Desktop with the Virtualization.framework backend is recommended. Apple Silicon pulls `arm64` images transparently. Windows 11 + WSL 2. Run every command from inside your WSL 2 distro, not PowerShell. Docker Desktop must have "Use WSL 2 based engine" enabled. Native Windows (no WSL) is not supported: path translation breaks bind mounts. Ubuntu 22.04+, Fedora 39+, or Arch. Install Docker Engine plus the Compose plugin. Rootless Docker works with caveats (see [Platform notes](#platform-notes)). Colima on macOS is supported with tweaks. See [Platform notes](#platform-notes). ## 2. Clone and configure ```bash theme={null} git clone https://github.com/TesslateAI/opensail.git cd opensail cp .env.example .env ``` Only two values are genuinely required for first boot; everything else has sensible defaults. | Variable | Why | How to get one | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | `SECRET_KEY` | Signs JWTs and derives other secrets. | `python -c "import secrets; print(secrets.token_hex(32))"` | | `LITELLM_API_BASE` and `LITELLM_MASTER_KEY` | Backend routes LLM calls through a LiteLLM proxy. Without a real endpoint, agent features stay disabled but the app still boots. | Point at your existing proxy, or stand up your own. | Open `.env.example` for the full list. The groups below are the ones you are most likely to touch. | Group | Vars | When to set | | ----------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Database | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_PORT` | Keep defaults for dev. Change `POSTGRES_PORT` only if `5432` is busy. | | Redis | `REDIS_URL`, `REDIS_PORT` | Default `redis://redis:6379/0` works in Compose. | | Secrets | `SECRET_KEY`, `INTERNAL_API_SECRET`, `CSRF_SECRET_KEY`, `DEPLOYMENT_ENCRYPTION_KEY`, `CHANNEL_ENCRYPTION_KEY` | Generate real values for any environment you share. `CHANNEL_ENCRYPTION_KEY` needs a Fernet key: `python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`. | | LiteLLM | `LITELLM_API_BASE`, `LITELLM_MASTER_KEY`, `LITELLM_DEFAULT_MODELS`, `LITELLM_TEAM_ID`, `LITELLM_INITIAL_BUDGET` | Required for the agent. `LITELLM_DEFAULT_MODELS` is a comma list, no spaces. | | OAuth (optional) | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_OAUTH_REDIRECT_URI`, `GITHUB_*` | Enable social login. Without these only email/password works. Redirect URI for dev: `http://localhost/api/auth/{google,github}/callback`. | | Ports | `APP_DOMAIN`, `APP_PROTOCOL`, `APP_PORT`, `BACKEND_PORT`, `FRONTEND_PORT`, `TRAEFIK_DASHBOARD_PORT` | Keep `APP_DOMAIN=localhost` for dev. Change individual ports only on conflicts. | | Stripe (optional) | `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_*_PRICE_ID` | Needed for billing UI. Use `sk_test_*` keys plus `stripe listen` for webhooks. | | SMTP (optional) | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_SENDER_EMAIL`, `TWO_FA_ENABLED` | Required for 2FA codes or password resets by email. | Docker Compose does not interpolate variables inside other variables in `.env`, so `ALLOWED_HOSTS=${APP_DOMAIN}` keeps the literal `${APP_DOMAIN}` string. For dev it still works because `APP_DOMAIN=localhost` is also the default. In production, set `ALLOWED_HOSTS` to an explicit value. ## 3. First boot ```bash theme={null} docker compose up --build -d ``` This builds the orchestrator and app images from source, pulls Postgres, Redis, and Traefik, and brings up seven services. The first build takes 3 to 6 minutes depending on your machine. Later boots are near-instant thanks to the build cache. The `devserver` service in `docker-compose.yml` exists purely to produce the `tesslate-devserver:latest` image that every user project container derives from. Its `entrypoint: true` keeps it from running, but the build still happens when Compose sees it. If `docker compose up` skipped the build (for example after a clean image prune), run it explicitly: ```bash theme={null} docker build -t tesslate-devserver:latest \ -f orchestrator/Dockerfile.devserver orchestrator/ ``` Without this image, creating or starting any user project fails with `pull access denied for tesslate-devserver`. ### Services | Service | Built from / image | Role | | -------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------- | | `traefik` | `traefik:v3.6` | Reverse proxy. Routes paths to the app or orchestrator and exposes `*.localhost` for user projects. | | `postgres` | `postgres:15-alpine` | Primary database. | | `redis` | `redis:7-alpine` | Pub/sub, ARQ task queue, distributed locks. | | `orchestrator` | `orchestrator/Dockerfile` | FastAPI backend. Mounts `/var/run/docker.sock` so it can spawn user containers. | | `worker` | Same image as `orchestrator` | Runs agent tasks off the ARQ queue. | | `gateway` | Same image as `orchestrator` | Persistent connections for Telegram, Slack, Discord, WhatsApp. Idle unless you enable channels. | | `app` | `app/Dockerfile` | Vite dev server with HMR. | | `devserver` | `orchestrator/Dockerfile.devserver` | Build-only image. Produces `tesslate-devserver:latest` that user project containers derive from. | ### Verify health ```bash theme={null} docker compose ps ``` Healthy output looks like this (timings vary): ``` NAME STATUS tesslate-app Up 30s (healthy) tesslate-gateway Up 28s tesslate-orchestrator Up 40s (healthy) tesslate-postgres-dev Up 45s (healthy) tesslate-redis Up 45s (healthy) tesslate-traefik Up 45s tesslate-worker Up 40s ``` The orchestrator can show `health: starting` for up to 30 seconds while Alembic migrations run. Tail it until Uvicorn reports ready: ```bash theme={null} docker compose logs -f orchestrator ``` ## 4. Seed the database On first backend startup the orchestrator automatically runs `run_all_seeds()` from [`orchestrator/app/seeds/__init__.py`](https://github.com/TesslateAI/opensail/blob/main/orchestrator/app/seeds/__init__.py). That covers themes, bases, agents, skills, MCP servers, and deployment targets on a clean database. Confirm: ```bash theme={null} docker compose exec postgres psql -U tesslate_user -d tesslate_dev \ -c "SELECT COUNT(*) FROM marketplace_agents;" ``` If the count is zero (older database or partial seed), re-run the scripts manually. Each is idempotent. ```bash macOS / Linux theme={null} # Copy seed scripts into the container once docker cp scripts/seed/. tesslate-orchestrator:/tmp/seed/ # Run in dependency order docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_themes.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_marketplace_bases.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_community_bases.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_marketplace_agents.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_opensource_agents.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_skills.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_mcp_servers.py docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_deployment_targets.py ``` ```bash Windows (Git Bash / MSYS2) theme={null} # Prefix every docker exec with MSYS_NO_PATHCONV=1 so /tmp paths are not translated MSYS_NO_PATHCONV=1 docker cp scripts/seed/. tesslate-orchestrator:/tmp/seed/ MSYS_NO_PATHCONV=1 docker exec -e PYTHONPATH=/app tesslate-orchestrator python /tmp/seed/seed_themes.py # ...repeat for the rest ``` What you get: themes (default-dark, default-light, midnight, ocean, forest, rose, sunset), official and open-source agents (Librarian, ReAct, Stream Builder), marketplace bases (Next.js, Vite+React+FastAPI, Vite+React+Go, Expo), open-source and Tesslate skills, MCP server catalog entries, and deployment targets (Vercel, Netlify, Cloudflare, Railway). ## 5. Access URLs | Target | URL | Notes | | ------------------------- | ------------------------------ | ----------------------------------------------------------------------------------- | | Frontend (via Traefik) | `http://localhost` | Use this for OAuth and cookie-correct testing. | | Frontend direct | `http://localhost:5173` | Vite dev server. Bypasses Traefik. | | Backend API (via Traefik) | `http://localhost/api` | Frontend calls here. | | Backend direct | `http://localhost:8000` | Useful for `curl`. | | OpenAPI docs | `http://localhost:8000/docs` | Swagger UI. | | Traefik dashboard | `http://localhost:8080` | Raw dashboard. | | Traefik via proxy | `http://localhost/traefik` | Basic-auth gated. Defaults to `admin:admin`. Change `TRAEFIK_BASIC_AUTH` in `.env`. | | PostgreSQL | `localhost:5432` | Connect with pgAdmin or DBeaver. Creds from `.env`. | | Redis | `localhost:6379` | `redis-cli -h localhost` works. | | User project | `http://{container}.localhost` | Wildcard is handled by Traefik. Some OS need `dnsmasq` or `/etc/hosts` entries. | ## 6. Create your first user 1. Visit `http://localhost`. 2. Click "Sign up", enter email and password. 3. You are logged in. Billing starts on the FREE tier. ```bash theme={null} docker compose exec orchestrator python /app/create_superuser.py ``` The script prompts interactively for email and password. To promote an existing user to admin: ```bash theme={null} docker compose exec orchestrator python /app/make_admin.py you@example.com ``` Scripts live at [`orchestrator/create_superuser.py`](https://github.com/TesslateAI/opensail/blob/main/orchestrator/create_superuser.py) and [`orchestrator/make_admin.py`](https://github.com/TesslateAI/opensail/blob/main/orchestrator/make_admin.py). ## 7. Create your first project From the dashboard, click "New project". Pick a base such as "Vite + React + FastAPI", give it a name, and confirm. A slug like `my-app-k3x8n2` is generated. The orchestrator copies the template and writes a `docker-compose.yml` into `/projects/{slug}/`. Watch for the "Project ready" toast. Click "Start". Containers spin up on `tesslate-network` and register with Traefik. The preview panel loads `http://frontend.localhost` (or whatever the base's primary container is called). Open the chat panel and ask the agent to make a change. For the complete agent tool reference (read/write, bash, sessions, web search, skills, schedules), see the [tesslate-agent reference](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). ## 8. Clean slate reset This drops every OpenSail container, volume, and image. Use it when a local state gets stuck. ```bash theme={null} # 1. Stop and remove containers plus volumes docker compose down --volumes --remove-orphans # 2. Remove all OpenSail images docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" \ | grep -i tesslate \ | awk '{print $2}' \ | sort -u \ | xargs -r docker rmi -f # 3. Rebuild and start docker compose up --build -d ``` Leave out step 2 if you only want to reset the database; step 1 already wipes `tesslate-postgres-dev-data`, `tesslate-redis-data`, `tesslate-projects-data`, `tesslate-base-cache`, and `tesslate-gateway-locks`. Database-only reset: ```bash theme={null} docker compose down docker volume rm tesslate-postgres-dev-data docker compose up -d ``` ## Quick Start For someone who has already read this guide once: ```bash theme={null} git clone https://github.com/TesslateAI/opensail.git cd opensail cp .env.example .env # edit SECRET_KEY and LITELLM_* in .env docker compose up --build -d docker compose ps # wait for healthy open http://localhost # macOS; xdg-open on Linux, start on Windows ``` ## Platform notes ### WSL 2 (Windows) * Clone into the WSL filesystem (for example `~/code/opensail`). Bind mounts from `/mnt/c/...` are slow and drop file-change events. * Run `docker compose` from inside WSL, not from PowerShell. * When piping scripts that pass container paths through `docker exec`, prefix with `MSYS_NO_PATHCONV=1` on Git Bash. ### macOS with Colima Colima replaces Docker Desktop on macOS. OpenSail works with two tweaks: ```bash theme={null} colima start --cpu 4 --memory 8 --disk 60 --mount-type virtiofs export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" ``` virtiofs is the only mount type fast enough to keep Vite HMR events responsive. ### Linux rootless Docker The orchestrator bind-mounts the Docker socket so it can manage user project containers. Under rootless Docker the socket path is `$XDG_RUNTIME_DIR/docker.sock`, and the orchestrator inside the container cannot see it at `/var/run/docker.sock`. Either run rootful Docker for development, or change the volume mount in `docker-compose.yml` to the rootless socket and set `DOCKER_HOST` inside the orchestrator. Kubernetes mode sidesteps this entirely. ### Volume permissions On Linux, the Postgres volume is owned by UID 70 (the alpine postgres user). If you shell in as a different UID you may see permission errors writing to `/var/lib/postgresql/data`. Do not chown the volume from the host; let the container manage it. ## Troubleshooting Symptom: `bind: address already in use` on `80`, `5432`, `6379`, `8000`, `5173`, or `8080`. Override the port in `.env`: ```env theme={null} APP_PORT=8081 BACKEND_PORT=8001 FRONTEND_PORT=5174 POSTGRES_PORT=5433 REDIS_PORT=6380 TRAEFIK_DASHBOARD_PORT=8090 ``` Re-run `docker compose up -d`. If you changed `APP_PORT`, Traefik dashboard moves with it, so use `http://localhost:8081`. ```bash theme={null} docker compose logs --tail 100 orchestrator ``` Usual causes: * Postgres not ready yet: wait 15 more seconds. * `SECRET_KEY` is empty or still at the placeholder. * `LITELLM_API_BASE` unreachable: boot continues but the log shows warnings. * Port 8000 busy on the host: change `BACKEND_PORT`. Modern Linux (systemd-resolved), macOS, and Windows with WSL 2 resolve `*.localhost` to `127.0.0.1` automatically. Some distros do not. * Linux: add `address=/localhost/127.0.0.1` to dnsmasq, or add per-project entries to `/etc/hosts`. * Windows native: edit `C:\Windows\System32\drivers\etc\hosts`. * Chrome and Firefox honor loopback for `*.localhost` without `/etc/hosts`. The compose file already sets `WATCHFILES_FORCE_POLLING=true`, `CHOKIDAR_USEPOLLING=true`, and `WATCHPACK_POLLING=true`. If it still stops working: * Inotify limit hit on Linux: `sudo sysctl fs.inotify.max_user_watches=524288`. * WSL 2: make sure the repo lives inside the WSL filesystem (`~/code/...`), not `/mnt/c/...`. The `/mnt` mount does not emit file events reliably. ```bash theme={null} docker compose ps postgres docker compose exec postgres pg_isready -U tesslate_user -d tesslate_dev ``` If the container is unhealthy, check `docker compose logs postgres`. Usually a leftover volume with a mismatched password; run the clean slate reset. * Traefik dashboard at `http://localhost:8080` lists every router. Confirm your container is there. * Verify the container has `com.tesslate.routable=true` (the orchestrator sets this automatically from the generated compose). * Check the container is on `tesslate-network`: `docker network inspect tesslate-network`. The orchestrator mounts `/var/run/docker.sock`. If you run rootless Docker, the socket lives in `$XDG_RUNTIME_DIR/docker.sock` and the mount is wrong. Either run rootful Docker or edit the volume mount in `docker-compose.yml`. ```bash theme={null} docker compose logs -f # everything docker compose logs -f orchestrator worker # just the Python services docker compose logs --tail 200 app # Vite last 200 lines ``` ## Next steps Install the native Tauri desktop app with local SQLite and per-project runtimes. Mirror production on Minikube with btrfs CSI and Volume Hub. Deploy OpenSail to AWS EKS with Terraform, ECR, and NGINX Ingress. Turn a project into a Tesslate App and list it in the marketplace. ## Getting help Chat with the OpenSail team and community. File bugs and feature requests. Direct support for teams and self-hosters. # Git Integration Source: https://docs.tesslate.com/guides/git-integration GitHub, GitLab, and Bitbucket OAuth; clone, commit, push, branch from inside OpenSail. Every project is a real git repo, every agent can read and write git state. Tesslate OpenSail ## Overview Every OpenSail project is a real git repository. You can import from GitHub, GitLab, or Bitbucket; push commits back; open PRs; and watch branches all from inside the studio. The agent has read access to git state through its git tools and write access through the shell, so it can create commits and push on your behalf (with approval). GitHub, GitLab, Bitbucket, all with OAuth and a unified API. Clone an existing repo, or start blank and push to a new remote. `git_status`, `git_log`, `git_blame`, `git_diff` are built-in agent tools. Every git command runs inside the project container, so `.gitignore`, LFS, and hooks all work. ## Connecting a provider Go to Settings, then Git Providers. Click Connect on GitHub, GitLab, or Bitbucket. You'll be redirected to the provider's OAuth consent page. OpenSail requests the minimum scopes needed for the provider: * GitHub: `repo`, `user:email`, `read:user` * GitLab: `api`, `read_user` * Bitbucket: `repository`, `account` The token is stored encrypted in `GitProviderCredential` and refreshed automatically when it expires. You can connect more than one provider. OpenSail matches the right credential to the repo based on the host in the clone URL. ## Importing a repo Dashboard, then New Project. Choose GitHub, GitLab, or Bitbucket as the source. Browse your repos (including orgs you belong to). Filter by org, search by name. Default branch is auto-selected. Override if you want to start from a feature branch. OpenSail clones into a new project container and runs setup. The clone happens in a background task. The UI shows progress while the container initializes. ## Branching and committing The Git panel shows the current branch, ahead/behind count, uncommitted changes, and remotes. Buttons: * Create branch * Commit (stages and commits tracked changes) * Push * Pull * Switch branch Open the in-project terminal and use git normally: ```bash theme={null} git checkout -b feature/add-billing git add . git commit -m "Add billing page" git push -u origin feature/add-billing ``` The terminal runs inside the project container, so your provider credentials are pre-authenticated for you. Ask the agent to commit and push. It uses shell tools for writes and git-read tools to verify. In Ask Before Edit mode (default), it pauses for approval before `git push`. Example: "commit everything as 'rework onboarding' and push". ## Agent-side git The agent has four read-only git tools built in: | Tool | Use | | ------------ | ------------------------------------------------ | | `git_status` | What's staged, what's modified, what's untracked | | `git_log` | Commit history for the current branch | | `git_blame` | Per-line authorship on a file | | `git_diff` | Unified diff for working tree or between refs | Writes go through the shell (`bash_exec`). This means normal git hooks, LFS, and `.gitignore` rules all apply. The approval system gates destructive commands: `git push --force`, `git reset --hard`, and similar are gated in Ask Before Edit mode. ## Multi-provider support OpenSail abstracts the three providers behind a single API: | Provider | Clone URL | OAuth | | --------- | ---------------------------------------------------- | --------------- | | GitHub | `https://github.com/user/repo.git` | GitHub App | | GitLab | `https://gitlab.com/user/repo.git` (and self-hosted) | GitLab OAuth | | Bitbucket | `https://bitbucket.org/user/repo.git` | Bitbucket OAuth | When the agent or the UI runs `git clone` or `git push`, the credential matcher looks at the host and selects the right stored token automatically. ## Pushing to a new remote Starting from a blank template and want to create a new remote? Git panel, then Create Remote. Pick GitHub, GitLab, or Bitbucket. Enter the name and visibility (public or private). OpenSail calls the provider API to create the repo with your token, wires it as `origin`, and pushes the initial branch. ## Pull requests PRs are not created automatically on push. Open them from the provider UI. OpenSail's Git panel has a Open on GitHub/GitLab/Bitbucket button that takes you straight to the compare view for the current branch. PR automation (creating PRs from the studio, commenting, approving) is on the roadmap. Today you manage PRs on the provider site. ## Merging and syncing The Git panel shows behind count when the remote has moved ahead. Click Pull to fast-forward or merge. Conflicts surface in Monaco with inline markers. Open the terminal and run `git fetch origin && git merge origin/main`. The agent can also do this on request. Monaco highlights conflict markers. Edit to resolve, save, then commit. The agent can also handle small conflicts: paste the conflict output and ask for help. ## .gitignore and large files Standard `.gitignore` works. The default template adds `node_modules`, `dist`, `.env`, `.tesslate/cache`. The Tesslate config file (`.tesslate/config.json`) is committed so the graph travels with the repo. For large files, use Git LFS. `git lfs install` inside the terminal once, then `git lfs track` normally. The CSI volume has plenty of room for LFS objects; they persist with the workspace snapshot. ## Secrets, tokens, and safety Never commit secrets. `.env` is in the default `.gitignore`. Use environment variables in project settings or a secrets manager connector (see [Connectors (MCP)](/guides/connectors-mcp)). OAuth tokens for connected providers are encrypted at rest and never exposed to the agent. When the agent runs git commands, it sees only pre-populated credential helpers, not the raw token. ## Revoking a provider Find the connected provider. Click Disconnect. The token is deleted from `GitProviderCredential`. Visit the provider's OAuth apps list and revoke the OpenSail app to be extra safe. ## Troubleshooting Token likely expired or the repo scopes changed. Reconnect the provider and retry. Make sure the OAuth app has access to that org. On GitHub, check the GitHub App installation and grant access to the org. Configure `git config user.name` and `git config user.email` inside the project terminal. The agent will also ask if the author is missing before committing. Large repos take longer. You can shallow-clone with `--depth=1` via the terminal. The default clone gets full history. ## Best practices The agent works best with a clean branch. Start a branch per task, PR when done. Check in `.tesslate/config.json`. Teammates and forks get the same graph. Ask the agent to run `git_diff` before writing a commit message. It writes better messages when it sees the diff. Pre-commit hooks run normally (inside the container). Use them to enforce linting and tests. ## Related Import from git as part of project creation. Where conflicts get resolved. Let the agent drive commits and branches. Run git operations through the external agent API. # Kanban Board Source: https://docs.tesslate.com/guides/kanban-board Track tasks, plan work, and let agents pick up tickets. Every card has a TSK-NNNN ref and lives inside your project. Agents can create, move, and comment as they work. Tesslate OpenSail ## Work lives inside the project Every project has a built-in Kanban board. Columns, cards, and comments live in the project database, so the agent can read and write them with the same authority it edits files. No separate tracker, no context-switching to Linear or Jira for simple task management. Every card gets a human-readable ref. `TSK-0001`, `TSK-0042`. Agents cite them in commits, comments, and commit messages. Move cards between columns by dragging. Reorder within a column too. Agents can create cards, move them, update status, and comment. The board is a shared tool. Due dates, assignees, labels, threaded comments, markdown descriptions. ## Opening the board Open the Kanban panel from the tool dock in the project builder. It renders in `panels/KanbanPanel.tsx` with columns, cards, and inline editors. Default columns: * **Backlog** Ideas and future work * **Todo** Ready to pick up * **In Progress** Actively being worked on * **Review** Waiting for review or validation * **Done** Completed Columns are fully configurable. Rename, reorder, add, or remove columns to match your team's workflow. ## Creating a card Inline input opens. Type a title and hit Enter. Click the card to open the side panel. Add description (markdown), due date, assignee, labels, and links. Thread-style comments appear under the description. Agents post comments too. ## TSK-NNNN refs Every card gets a unique ref starting at `TSK-0001`. Refs never renumber when cards are deleted, so they stay stable forever. Use them everywhere: * **In commit messages** `git commit -m "TSK-0042: add Slack OAuth"` * **In chat** "TSK-0042 is blocked on the API token" * **In PR descriptions** Link them for traceability * **In code comments** `// TSK-0042: revisit when the rate limit lifts` The agent's kanban tool takes refs as input, so "move TSK-0042 to Review" works from chat. ## The agent's kanban tool The agent has a built-in `kanban` tool with these operations: | Operation | Purpose | | --------- | --------------------------------------------------- | | `create` | Create a new card in a column | | `move` | Move a card to a different column | | `update` | Edit title, description, labels, due date, assignee | | `comment` | Post a comment on a card | | `get` | Read a card by ref | | `list` | List cards in a column or across columns | Common patterns: * **Spawn from description** "Break this project spec into tickets for the backlog." The agent reads your description, creates one card per subtask, and organizes them into columns. * **Close as you work** Ask the agent to implement TSK-0042. It reads the card, implements, creates a commit, moves the card to Review, and comments with the commit SHA. * **Plan and track** The agent's `update_plan` tool pairs well with kanban. Use the plan for the current session, kanban for longer-lived work. The agent respects edit mode when writing to the board. In Ask mode, card mutations show an `ApprovalRequestCard` just like file writes. ## Filtering and search The board top bar has: * **Search** Filter cards by title, description, or ref * **Label filter** Show only cards with specific labels * **Assignee filter** Show only cards assigned to a specific user * **Due date filter** Overdue, this week, no date Filters combine. URL params persist filter state so you can bookmark a view. ## Columns Click the + at the end of the column list. Name it, save. Click the column title to rename inline. Drag the column header to reorder. Kebab menu on the column. Cards in the column move to the first remaining column. ## Comments Comments are threaded under each card. Markdown is supported. When the agent comments, it is labeled with the agent's name and icon so you can distinguish agent activity from human activity. Mentions (`@user`) notify team members. The agent does not mention by default, but you can configure it to mention specific humans on status changes through a skill. ## Labels Labels are color-coded tags. Common setups: * **Priority** `P0`, `P1`, `P2`, `P3` * **Area** `frontend`, `backend`, `infra`, `docs` * **Type** `bug`, `feature`, `chore`, `tech-debt` Create labels in the board settings. Apply them to any card from the detail panel. ## Integration with the agent The kanban board lives in the same project as your code, so the agent has full read/write. Some patterns that work well: A scheduled agent reads In Progress and Review columns, pulls recent commits, and posts a standup summary to Slack every morning. A skill teaches the agent your prioritization rules ("always pick P0 first, then oldest"). The agent picks one card, implements, moves to Review. Before implementation, ask the agent to list all cards matching a tag and summarize what's in scope for the current sprint. ## Keyboard shortcuts | Shortcut | Action | | -------------- | ----------------------------------------- | | `N` | New card in focused column | | `/` | Focus the search input | | `Enter` | Save inline edit | | `Esc` | Cancel inline edit | | `Cmd/Ctrl + K` | Command palette (includes kanban actions) | ## Related The agent that reads and writes the board. Ask the agent to work on TSK-NNNN refs. The project that owns the board. Reference TSK refs in commit messages. # Local Kubernetes (minikube) Source: https://docs.tesslate.com/guides/kubernetes-local Run Tesslate OpenSail on a local Kubernetes cluster with the btrfs CSI driver, Volume Hub, NGINX Ingress, MinIO, Postgres, and Redis. Tesslate OpenSail This guide walks you from an empty machine to a working OpenSail cluster on minikube. You will end up with the full production storage stack (btrfs CSI driver plus Volume Hub plus CAS to MinIO) and the same Ingress topology we run in AWS, but entirely on `http://localhost`. For the faster inner-loop dev flow without Kubernetes, use the [Docker Setup](/guides/docker-setup) guide instead. The K8s path is for contributors who need to reproduce storage, ingress, or snapshot bugs. Every `kubectl` command in this guide includes `--context=tesslate`. That is a hard project rule: background cronjobs and other processes can flip your active context mid-session, so the context must be pinned on every call. Never use `kubectl config use-context` or any context-switching helper. ## 1. What you will run Backend, frontend, worker, Postgres, and Redis in the `tesslate` namespace. Same manifests the cloud runs. Installed via the minikube `ingress` addon. Answers on `localhost` and `*.localhost` when `minikube tunnel` is running. Per-node btrfs subvolumes, instant snapshot-clone, and CAS sync to MinIO. Mirrors the AWS storage stack one-for-one. S3-compatible object store in the `minio-system` namespace. Backs content-addressable project snapshots. | Component | Namespace | Purpose | | ------------------- | --------------- | --------------------------------------------- | | OpenSail backend | `tesslate` | FastAPI orchestrator | | OpenSail frontend | `tesslate` | React UI served by NGINX | | PostgreSQL | `tesslate` | Primary database | | Redis | `tesslate` | Pub/sub and task queue | | MinIO | `minio-system` | S3-compatible object store | | btrfs CSI driver | `kube-system` | Per-node subvolumes and snapshots | | Volume Hub | `kube-system` | Volume orchestrator, cache placement, S3 sync | | Snapshot controller | `kube-system` | VolumeSnapshot CRDs | | NGINX Ingress | `ingress-nginx` | HTTP routing | Profile name is `tesslate`. Every `--context=tesslate` flag below refers to this profile. ## 2. Prerequisites `brew install minikube`, `choco install minikube`, or download from [minikube.sigs.k8s.io](https://minikube.sigs.k8s.io/docs/start/). `brew install kubectl` or `choco install kubernetes-cli`. [Docker Desktop](https://docs.docker.com/get-docker/) on macOS/Windows, or `docker-ce` on Linux. Used as the minikube driver. * CPU: 4 cores available to Docker * RAM: 8 GB available to Docker * Disk: 40 GB free for the minikube VM ### btrfs requirement The btrfs CSI driver needs btrfs inside the minikube VM. The Docker driver's base image already ships with `btrfs-progs` and the driver auto-creates its pool at `/mnt/tesslate-pool` inside the node. No host filesystem changes are required when using `--driver docker`. If you switch to kvm2 or hyperkit, make sure the guest image has `btrfs-progs` and a mountable btrfs partition. ### Hosts file Add these entries: * Linux / macOS: `/etc/hosts` * Windows: `C:\Windows\System32\drivers\etc\hosts` ``` 127.0.0.1 localhost 127.0.0.1 minio.localhost ``` Project container URLs follow `http://-.localhost` and are resolved by NGINX Ingress when `minikube tunnel --profile tesslate` is running. Modern browsers resolve `*.localhost` to `127.0.0.1` automatically; if yours does not, add the specific project host to `hosts` too. ## 3. Start the cluster ```bash Start the minikube profile theme={null} minikube start \ --profile tesslate \ --cpus 4 \ --memory 8g \ --disk-size 40g \ --driver docker \ --addons ingress \ --addons storage-provisioner \ --addons metrics-server ``` ```bash Verify the cluster theme={null} kubectl --context=tesslate get nodes kubectl --context=tesslate get pods -n ingress-nginx ``` ```bash Start the tunnel (leave running) theme={null} minikube tunnel --profile tesslate ``` The `ingress` addon installs NGINX Ingress in the `ingress-nginx` namespace. The tunnel exposes the Ingress controller on `127.0.0.1`; keep that terminal open while you use the cluster. ## 4. Install snapshot controller and btrfs CSI driver OpenSail depends on Kubernetes `VolumeSnapshot` resources for hibernation and project timeline. Install the CRDs and controller first, then build and deploy the btrfs CSI driver. ```bash theme={null} SNAP_VERSION=v8.2.0 CRD_BASE="https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/${SNAP_VERSION}/client/config/crd" CTRL_BASE="https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/${SNAP_VERSION}/deploy/kubernetes/snapshot-controller" kubectl --context=tesslate apply -f ${CRD_BASE}/snapshot.storage.k8s.io_volumesnapshotclasses.yaml kubectl --context=tesslate apply -f ${CRD_BASE}/snapshot.storage.k8s.io_volumesnapshotcontents.yaml kubectl --context=tesslate apply -f ${CRD_BASE}/snapshot.storage.k8s.io_volumesnapshots.yaml kubectl --context=tesslate apply -f ${CTRL_BASE}/rbac-snapshot-controller.yaml kubectl --context=tesslate apply -f ${CTRL_BASE}/setup-snapshot-controller.yaml ``` ```bash theme={null} docker build -t tesslate-btrfs-csi:latest -f services/btrfs-csi/Dockerfile services/btrfs-csi/ minikube -p tesslate image load tesslate-btrfs-csi:latest ``` The minikube overlay at `services/btrfs-csi/overlays/minikube/` ships an example secrets file. Copy it first: ```bash theme={null} cp services/btrfs-csi/overlays/minikube/csi-credentials.example.yaml \ services/btrfs-csi/overlays/minikube/csi-credentials.yaml ``` Edit `csi-credentials.yaml` only if you change the MinIO admin password. The default value matches the example MinIO secret you will edit in the next section. Both secrets must agree. ```bash theme={null} kubectl --context=tesslate apply -k services/btrfs-csi/overlays/minikube kubectl --context=tesslate rollout status daemonset/tesslate-btrfs-csi-node -n kube-system --timeout=120s kubectl --context=tesslate rollout status deployment/tesslate-volume-hub -n kube-system --timeout=120s ``` This installs: * `tesslate-btrfs-csi-node` DaemonSet (one pod per node) * `tesslate-volume-hub` Deployment (Hub plus CSI provisioner / snapshotter sidecars) * `tesslate-image-precache` DaemonSet (pre-pulls the devserver image on every node) ### What Volume Hub does Volume Hub is the storageless orchestrator that sits above the per-node btrfs CSI driver. It exposes a gRPC API at `tesslate-volume-hub.kube-system.svc:9750` and the backend talks to it through `orchestrator/app/services/hub_client.py`. | RPC | What it does | | -------------- | ------------------------------------------------------------------------------ | | `CreateVolume` | Pick a node with capacity, create an empty or template-cloned subvolume | | `EnsureCached` | Guarantee a volume is present on the node where a pod is about to be scheduled | | `TriggerSync` | Push CAS content to MinIO when a project hibernates | | `DeleteVolume` | Remove the subvolume and any S3 objects it owns | The minikube overlay patches `imagePullPolicy: Never` so the locally loaded `tesslate-btrfs-csi:latest` image is always used, even if a registry rebuild changes the `:latest` digest upstream. ## 5. Configure secrets Each cluster secret has a `*.example.yaml` under `k8s/overlays/minikube/secrets/`. Copy and edit every file. ```bash theme={null} cd k8s/overlays/minikube/secrets cp app-secrets.example.yaml app-secrets.yaml cp postgres-secret.example.yaml postgres-secret.yaml cp s3-credentials.example.yaml s3-credentials.yaml cp minio-credentials.example.yaml minio-credentials.yaml cd - ``` | File | Key | Notes | | ------------------------ | ------------------------------------------ | ---------------------------------------------------------------------------------------- | | `app-secrets.yaml` | `SECRET_KEY` | `python -c "import secrets; print(secrets.token_hex(32))"` | | `app-secrets.yaml` | `INTERNAL_API_SECRET` | Same generator; must match `ORCHESTRATOR_INTERNAL_SECRET` in `tesslate-btrfs-csi-config` | | `app-secrets.yaml` | `DATABASE_URL` | `postgresql+asyncpg://tesslate_user:@postgres:5432/tesslate_dev` | | `app-secrets.yaml` | `LITELLM_API_BASE`, `LITELLM_MASTER_KEY` | Your LiteLLM proxy or OpenAI-compatible endpoint | | `postgres-secret.yaml` | `POSTGRES_PASSWORD` | Must match the password embedded in `DATABASE_URL` | | `s3-credentials.yaml` | `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | Must match `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` | | `minio-credentials.yaml` | `MINIO_ROOT_USER`, `MINIO_ROOT_PASSWORD` | MinIO admin credentials | Leave OAuth, Stripe, and SMTP blank unless you need those flows during local development. The backend boots fine with empty values. The seeded `crm-demo` and `nightly-digest` apps reference a cluster secret called `llama-api-credentials`. Without it, those pods fail to start. ```bash theme={null} kubectl --context=tesslate -n tesslate create secret generic llama-api-credentials \ --from-literal=api_key='' ``` OAuth callbacks in the example secrets point at `http://localhost/api/auth//callback`. Providers that enforce HTTPS callbacks will not work against pure minikube. Use Cloudflare Tunnel (`k8s/overlays/minikube/cloudflare-tunnel/`) if you need to exercise them. ## 6. Deploy OpenSail ```bash theme={null} docker build -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/ docker build -t tesslate-frontend:latest -f app/Dockerfile.prod app/ docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver . minikube -p tesslate image load tesslate-backend:latest minikube -p tesslate image load tesslate-frontend:latest minikube -p tesslate image load tesslate-devserver:latest ``` The minikube overlay sets `K8S_DEVSERVER_IMAGE=tesslate-devserver:latest` and `K8S_IMAGE_PULL_POLICY=Never`, so the devserver image must already be inside the node before any user project starts. ```bash theme={null} kubectl --context=tesslate apply -k k8s/overlays/minikube/minio kubectl --context=tesslate wait --for=condition=ready pod -l app=minio -n minio-system --timeout=180s ``` The init job creates two buckets: `tesslate-projects` (used by the backend) and `tesslate-btrfs-snapshots` (used by the CSI driver's CAS sync). ```bash theme={null} kubectl --context=tesslate apply -k k8s/overlays/minikube ``` This pulls in everything from `k8s/base/` (namespace, backend, frontend, Postgres, Redis, Ingress, security, Volume Hub references) and applies the minikube-specific patches (local images, `imagePullPolicy: Never`, HTTP-only Ingress, single replicas). ```bash theme={null} kubectl --context=tesslate rollout status deployment/postgres -n tesslate --timeout=180s kubectl --context=tesslate rollout status deployment/redis -n tesslate --timeout=120s kubectl --context=tesslate rollout status deployment/tesslate-backend -n tesslate --timeout=300s kubectl --context=tesslate rollout status deployment/tesslate-frontend -n tesslate --timeout=180s ``` The backend runs Alembic migrations on startup. If the first pod fails with a migration error it will retry; give it a minute before investigating. If all four rollouts succeed, OpenSail is live. Keep the `minikube tunnel` terminal open. ## 7. Access the app With the tunnel running, NGINX Ingress answers on `localhost`. No port-forwarding needed for normal use. | URL | What it serves | | -------------------------------------- | ----------------------------------------------------------------------------- | | `http://localhost/` | Frontend | | `http://localhost/api/` | Backend API | | `http://-.localhost/` | User project preview (for example, `http://my-app-k3x8n2-frontend.localhost`) | | `http://minio.localhost/` | MinIO S3 API (after the Ingress rule or via port-forward below) | Without the tunnel, use port-forwards: ```bash theme={null} kubectl --context=tesslate port-forward -n tesslate svc/tesslate-frontend-service 5000:80 kubectl --context=tesslate port-forward -n tesslate svc/tesslate-backend-service 8000:8000 kubectl --context=tesslate port-forward -n minio-system svc/minio 9001:9001 ``` OpenSail does not run Traefik in Kubernetes. Traefik is only the Docker Compose dev mode router; NGINX Ingress serves the same role on minikube and in AWS. ## 8. Seed the database ```bash Loop through the seed scripts theme={null} BACKEND_POD=$(kubectl --context=tesslate get pods -n tesslate \ -l app=tesslate-backend -o jsonpath='{.items[0].metadata.name}') for script in \ seed_marketplace_bases.py \ seed_marketplace_agents.py \ seed_opensource_agents.py \ seed_skills.py \ seed_themes.py \ seed_mcp_servers.py \ seed_community_bases.py do kubectl --context=tesslate cp "scripts/seed/$script" "tesslate/${BACKEND_POD}:/tmp/$script" kubectl --context=tesslate exec -n tesslate "$BACKEND_POD" -- python "/tmp/$script" done ``` ```bash Single script theme={null} kubectl --context=tesslate cp scripts/seed/seed_marketplace_bases.py \ tesslate/${BACKEND_POD}:/tmp/seed_marketplace_bases.py kubectl --context=tesslate exec -n tesslate ${BACKEND_POD} -- python /tmp/seed_marketplace_bases.py ``` Individual failures are non-fatal; the remaining scripts still run. Every seed script is idempotent, so it is safe to re-run. ## 9. Create a project From `http://localhost/`, sign up and create a project. Behind the scenes the backend: 1. Creates a namespace `proj-` with a NetworkPolicy isolating it from other project namespaces. 2. Asks Volume Hub to pick a node with capacity and provision a btrfs subvolume, cloning from the template snapshot if one exists. 3. Creates a PVC bound to storage class `tesslate-btrfs`. PVC size defaults to `K8S_PVC_SIZE=5Gi`. 4. Creates one Deployment and Service per container declared in the project's `.tesslate/config.json`. Multiple containers are kept on the same node via pod affinity so they share the volume without cross-node traffic. 5. Adds an Ingress rule per exposed container on `http://-.localhost`. Inspect what landed: ```bash theme={null} kubectl --context=tesslate get ns | grep proj- NS= kubectl --context=tesslate get all,pvc,ingress -n $NS kubectl --context=tesslate describe pod -n $NS -l app=frontend ``` ## 10. Snapshots and hibernation OpenSail keeps up to `K8S_MAX_SNAPSHOTS_PER_PROJECT=5` VolumeSnapshot objects per project as a rolling timeline. Idle projects hibernate after `K8S_HIBERNATION_IDLE_MINUTES=10` minutes: the backend calls Volume Hub `TriggerSync` to push the CAS content to MinIO, then tears down the compute pod while keeping the volume cached on its node. ```bash Watch snapshots and volume state theme={null} NS= kubectl --context=tesslate get volumesnapshots -n $NS kubectl --context=tesslate get volumesnapshotcontents kubectl --context=tesslate get pvc -n $NS ``` ```bash Watch Volume Hub logs theme={null} kubectl --context=tesslate logs -n kube-system deploy/tesslate-volume-hub -c hub --tail=100 -f ``` When you click Start on a hibernated project, Volume Hub uses the fast path if the cached subvolume is still on its node, or peer-transfers or restores from MinIO otherwise. ## 11. Common commands Always include `--context=tesslate`. ```bash Status theme={null} kubectl --context=tesslate get pods -n tesslate kubectl --context=tesslate get pods -A | grep proj- kubectl --context=tesslate get events -n tesslate --sort-by='.lastTimestamp' ``` ```bash Logs theme={null} kubectl --context=tesslate logs -f deployment/tesslate-backend -n tesslate kubectl --context=tesslate logs -f deployment/tesslate-frontend -n tesslate kubectl --context=tesslate logs -n kube-system deploy/tesslate-volume-hub -c hub ``` ```bash Shell and port-forward theme={null} kubectl --context=tesslate exec -it deployment/tesslate-backend -n tesslate -- /bin/bash kubectl --context=tesslate port-forward -n tesslate svc/tesslate-backend-service 8000:8000 ``` ```bash Rebuild and restart backend theme={null} minikube -p tesslate ssh -- docker rmi -f tesslate-backend:latest docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/ minikube -p tesslate image load tesslate-backend:latest kubectl --context=tesslate delete pod -n tesslate -l app=tesslate-backend ``` On Windows Git Bash, prefix `kubectl` and `docker exec` calls with `MSYS_NO_PATHCONV=1` so paths are not mangled. Use `kubectl delete pod` instead of `kubectl rollout restart` when swapping an image with the same tag. Rollout restart can reuse cached image layers; deleting the pod forces a fresh pull from the node's Docker cache. ## 12. Teardown ```bash Remove app resources, keep the cluster theme={null} ./k8s/scripts/minikube/teardown.sh ``` ```bash Manual cleanup theme={null} kubectl --context=tesslate delete namespace tesslate --ignore-not-found kubectl --context=tesslate delete namespace minio-system --ignore-not-found kubectl --context=tesslate delete storageclass tesslate-block-storage --ignore-not-found ``` ```bash Destroy the entire cluster theme={null} ./k8s/scripts/minikube/teardown.sh --all # or minikube delete --profile tesslate ``` Destroying the cluster loses all user project data. PVCs survive a `minikube stop` / `minikube start` cycle; they do not survive `minikube delete`. ## 13. Troubleshooting The minikube VM image must have btrfs tools available. Use `--driver docker` (confirmed working) and check the init container: ```bash theme={null} kubectl --context=tesslate describe pod -n kube-system -l app=tesslate-btrfs-csi-node kubectl --context=tesslate logs -n kube-system -l app=tesslate-btrfs-csi-node -c init-btrfs ``` If you changed driver, the safest fix is to recreate the cluster with the Docker driver. Symptom: `no matches for kind "VolumeSnapshot" in version "snapshot.storage.k8s.io/v1"`. Re-run section 4. ```bash theme={null} kubectl --context=tesslate get crds | grep snapshot.storage.k8s.io kubectl --context=tesslate get pods -n kube-system -l app=snapshot-controller ``` 1. Confirm `minikube tunnel --profile tesslate` is still running in another terminal. 2. `kubectl --context=tesslate get ingress -A | grep proj-` and verify the rule exists. 3. `kubectl --context=tesslate describe ingress -n proj-` and look for controller errors. If your OS does not resolve `*.localhost` automatically, add the specific host to `/etc/hosts` pointing at `127.0.0.1`. Local images were not loaded into the minikube node, or the tag drifted. ```bash theme={null} minikube -p tesslate image ls | grep tesslate minikube -p tesslate image load tesslate-backend:latest kubectl --context=tesslate rollout restart deployment/tesslate-backend -n tesslate ``` ```bash theme={null} kubectl --context=tesslate describe pod -n kubectl --context=tesslate logs -n --previous ``` Common causes: wrong `DATABASE_URL` in `app-secrets.yaml`, Postgres not ready yet, `INTERNAL_API_SECRET` mismatch between `tesslate-app-secrets` and `tesslate-btrfs-csi-config`, or missing `llama-api-credentials` for seeded apps. ```bash theme={null} kubectl --context=tesslate delete pvc postgres-pvc -n tesslate kubectl --context=tesslate delete pod -l app=postgres -n tesslate kubectl --context=tesslate rollout restart deployment/tesslate-backend -n tesslate ``` The orchestrator re-runs Alembic migrations on the next boot and you can re-seed the database. ## Next steps Deploy OpenSail to EKS with NLB ingress, cert-manager, and Cloudflare DNS. Faster inner-loop dev without Kubernetes. Same code paths, different orchestrator. Package and publish an app to the Tesslate marketplace from your local cluster. High-level tour of how the orchestrator, agents, and runtimes fit together. ## Getting help Real-time help from the Tesslate community. Source, issues, and release notes. Direct support at `support@tesslate.com`. # Live Preview Source: https://docs.tesslate.com/guides/live-preview Iframe preview with hot module reload per project. Preview nodes on the Architecture Panel, responsive breakpoints, and per-container URLs for multi-service apps. Tesslate OpenSail ## How it works The live preview is an iframe rendered next to the code editor and the Design canvas. The iframe points at your running dev server. Every save triggers hot module reload (HMR) inside the container, the dev server pushes the update, and the iframe repaints. No polling, no refresh buttons. Save, see it. Your framework's hot reload flows straight into the preview iframe. Multi-service projects get a URL per container. The Architecture Panel renders live preview windows as graph nodes. Test 375px up to 1536px from a breakpoint switcher. ## URL pattern Container URLs differ by runtime. `http://127.0.0.1:{port}` direct loopback. No reverse proxy. `http://{container}.localhost` or `http://{container}.{slug}.localhost`. Traefik routes by hostname to the right container on a shared Docker network. `https://{container}.{slug}.{domain}` in production (for example `https://frontend.my-app-k3x8n2.opensail.tesslate.com`). NGINX Ingress routes by hostname with a wildcard TLS certificate. Minikube uses `http://{container}.localhost` for local dev. `GET /api/projects/{slug}/dev-server-url` returns the base URL the frontend uses to build per-container iframe URLs. ## The iframe The preview iframe is wrapped in a mini-browser with: * **URL bar** showing the active container URL * **Back/forward/home/refresh** for navigating multi-page apps * **Copy link** for sharing or opening in a standalone tab * **Fullscreen** to dedicate the whole workspace to the preview If the dev server is not yet ready, the preview shows a startup overlay with pulsing grid spinner, progress bar, and streaming logs (`StartupLogViewer`). If the health check times out, an "Ask Agent" button kicks the agent off to diagnose and fix. ## Preview nodes on the canvas The Architecture Panel can host browser preview nodes alongside your containers. Drag the **Browser Preview** node from the Marketplace sidebar, drop it on the canvas, and connect it to a container with a `browser_preview` edge. What you get: * **Live iframe inside the canvas** with the same mini-browser chrome * **Resizable and repositionable** so you can arrange multi-viewport previews * **Drag to reposition** while the app keeps running * **One incoming connection** per preview node (enforced in `isValidConnection`) This is how you author multi-container apps while watching them run. ## Port picker for multi-container apps When your project has more than one previewable container, the `PreviewPortPicker` dropdown appears. It lets you switch the active iframe between the frontend, API docs, worker dashboard, or any container that exposes a port. Containers expose a port through the `effective_port` resolution chain: `internal_port` from config, then `port` column, then `3000` fallback. Always trust `effective_port`, never hardcode. ## Responsive breakpoints In Design view, the breakpoint switcher cycles through: | Breakpoint | Width | | ---------- | ----------------- | | **Fit** | Full canvas width | | **sm** | 640px | | **md** | 768px | | **lg** | 1024px | | **xl** | 1280px | | **2xl** | 1536px | | **Mobile** | 375px | The iframe width snaps to the chosen breakpoint and the canvas scales so the preview stays fully visible. ## Health checks Every container has a readiness probe that gates preview traffic. Until the dev server responds on the effective port, the iframe shows the startup overlay. When readiness passes, the iframe loads. On Kubernetes, readiness is HTTP-based (`GET /` on the port). Liveness is exec-based (tmux session health), so the pod stays alive even if the dev server crashes, giving the agent room to fix it. ## Manual reload Sometimes HMR does not cover every change (service workers, new env vars, infrastructure container restarts). Click the refresh button in the preview chrome for a hard reload. The design selection and canvas state persist across reloads. ## Preview security Preview URLs are authenticated. The dev server verifies the user's session through the `/auth/verify-access` endpoint before serving. Unauthenticated users hitting a preview URL get redirected to login. Team-visible projects also check team membership. ## Related The editor that sits beside the preview. Click-to-source editing on top of the iframe. Preview nodes on the canvas. Container lifecycle and URL patterns. # Marketplace Source: https://docs.tesslate.com/guides/marketplace Browse and install agents, skills, MCP servers, bases, themes, and apps. One place for every reusable building block on OpenSail. Tesslate OpenSail ## One marketplace, six item types The OpenSail marketplace is where the community and Tesslate publish every reusable thing on the platform. Six categories, one discovery surface, one install flow. Configured agent personalities with a system prompt, a tool subset, attached skills, and a preferred model. Reusable capabilities (review checklists, writing styles, deployment playbooks) that attach to any agent. Model Context Protocol integrations for Slack, Gmail, Linear, GitHub, Notion, and hundreds more. Starter project templates (Next.js 16, Vite+FastAPI, Expo, Go APIs, full-stack CRMs). Color, typography, spacing, and animation presets for the Studio UI and your projects. Versioned, immutable, installable bundles built from a workspace. Install and you get a running copy. ## Agents vs apps This is the most common source of confusion. An **agent** is a long-running persona. It lives inside your OpenSail instance. You can reconfigure it, attach new skills, change its model, and run it against any project. Installing an agent adds it to your library, not to a specific project. An **app** is an immutable, manifest-described bundle produced by freezing a workspace. Installing an app creates a new project with the same code, the same containers, the same graph, and the same permissions as the creator's source workspace. Every install is isolated. Apps are versioned (`AppVersion`) and the marketplace lists the latest approved version. | | Agent | App | | -------------- | ---------------------------- | ------------------------------------- | | **Lifecycle** | Long-running, reconfigurable | Install creates a new project | | **Mutability** | Editable in your library | Immutable per version | | **Scope** | Any project you open | One project per install | | **Versioning** | Updated by the creator | Semver per `AppVersion` | | **Provenance** | None | `forked_from` tracks fork trees | | **Approval** | Optional moderation | Staged pipeline before public listing | ## Browsing Click **Marketplace** in the sidebar. The home view shows featured and category carousels for agents, bases, skills, and MCP servers. Use the Browse route (`/marketplace/browse/:type`) for a paginated list with server-side filtering. Search by keyword, sort by popularity or recency, filter by author or tag. Every listing has a detail page with description, reviews, author profile, install button, and (for apps) a fork modal if the creator allows forking. One click installs into your library. For apps, the install saga provisions a new project from the app's volume template. For agents, skills, MCP servers, bases, and themes, the item lands in your library and is available project-wide. ## Installing Install flows differ by item type. Instant. The agent lands in `/library?tab=agents`. Configure name, icon, model, compaction model, context window, and thinking effort. Enable for a project by picking it in the chat agent selector. Instant. Skills land in `/library?tab=skills`. Attach to any agent via `AgentSkillAssignment` or enable project-wide via the skills panel. Requires OAuth for most servers. After OAuth, env vars are stored encrypted in `UserMcpConfig`. Attach to an agent via `AgentMcpAssignment`. Tool schemas are cached for 5 minutes and refresh on OAuth or server update. Used at project creation time. Pick a base in the Create Project wizard; the orchestrator clones the repo and seeds the workspace. Instant. Themes land in `/library?tab=themes`. Activate globally via Preferences or per-project via the theme picker. Runs through an install saga (`AppInstallAttempt` tracks every step for idempotency). Creates a new project, provisions the volume from the app's CAS bundle, and applies the manifest graph. Per-user billing dimensions (`AI compute`, `general compute`, `platform fee`) apply according to the creator's configuration. ## Apps: publish, install, fork Apps are the platform's distribution unit. Every app is a versioned, immutable bundle that started life as a workspace. **Publish** The creator runs through the publish wizard. The orchestrator freezes the workspace volume into a content-addressed bundle, builds an `AppVersion` with a manifest, and submits it to the approval pipeline. Private and team-only apps skip the public listing gate. **Approval pipeline** Public apps go through four stages: automated security scan (overbroad OAuth scopes, leaked secrets, dependency vulns), sandbox evaluation (synthetic inputs on a cheap model), human reviewer sign-off, and final publish. Each stage records individual `SubmissionCheck` results. Creators can appeal yanks. **Install** Every install creates a new project with its own volume, containers, and permissions. The wallet mix determines who pays for what (AI compute, general compute, platform fee), configured per-dimension (creator-pays, installer-pays, BYOK, platform-subsidized, promotional). **Fork** If the creator allows forking, anyone can fork an app. Fork creates a new workspace with full source access and a `forked_from` provenance link. The marketplace shows fork trees. **Bundles** An `AppBundle` groups multiple apps into a starter pack ("Lawyer Starter Pack" installs 10 apps with consolidated OAuth consent and a dashboard app at the center). ## Official vs community Tesslate publishes official agents, skills, bases, and themes. Community creators publish theirs under the same flow. Both are subject to the same approval process for public listing. Creator reputation (tracked in `AdminCreatorReputationPage`) gates distribution privileges at the edges. ## Creator tools If you want to publish, the Creator Studio (`/creator`) gives you: * **Drafts** Unpublished versions you are iterating on * **Submissions** Items in the approval pipeline with per-stage status * **Analytics** Installs, runs, revenue * **Billing** Your creator wallet and ledger Apps have their own publishing guide covering manifests, wallet mix, and approval. See [Publishing Apps](/guides/publishing-apps). ## Related Freeze a workspace into an immutable bundle. The agent runner that marketplace agents configure. How skills attach to agents. Installing and authenticating MCP servers. # Model Management Source: https://docs.tesslate.com/guides/model-management Route AI requests through LiteLLM, bring your own keys, or self-host models with Ollama or vLLM. Switch providers without rewriting agents. Tesslate OpenSail ## One agent, every model OpenSail is model-agnostic. Every AI call routes through a LiteLLM proxy, which exposes a unified OpenAI-compatible interface to any supported provider. Switch from Claude to GPT-4 to Qwen to a local Llama without changing a line of agent code. Unified proxy across all providers and local inference Per-token cost fetched live from the proxy and cached 5 minutes Attach your own keys; platform credits are not charged Point LiteLLM at Ollama, vLLM, or any OpenAI-compatible endpoint ## Supported providers OpenSail ships with first-party support for every major model provider. Any OpenAI-compatible endpoint also works out of the box. | Provider | Flagship models | | ------------------ | ------------------------------------------- | | **Anthropic** | Claude 3.5/4.x family (Opus, Sonnet, Haiku) | | **OpenAI** | GPT-4, GPT-4 Turbo, GPT-4o, o-series | | **DeepSeek** | DeepSeek V3, R1 | | **Meta** | Llama 3.1, Llama 3.2, Llama 3.3 | | **Mistral** | Mistral Large, Codestral, Mixtral | | **Qwen** | Qwen 2.5, Qwen-Coder, QwQ | | **Google** | Gemini 1.5/2.x Pro, Flash | | **Moonshot** | Kimi K2 family | | **MiniMax** | MiniMax M-series | | **Z.AI (ChatGLM)** | GLM-4 family | | **xAI** | Grok | Additional gateways: OpenRouter (100+ models through a single key), Groq (ultra-low latency), Together, Fireworks, Perplexity, Cohere, Bedrock. Any gateway LiteLLM can reach is routable. ## How routing works The tesslate-agent's LLM adapter calls the LiteLLM proxy with an OpenAI-style chat completion request. The `check_credits(user, model_name)` function runs before the upstream request. If the model is BYOK for this user, the check always passes. Otherwise it verifies sufficient platform credits across all pools. LiteLLM looks up the model name, resolves the provider, and dispatches to the correct upstream API using either the platform's key, your BYOK key (passthrough mode), or a local endpoint. The response streams back through the proxy to the agent, token by token, into the chat UI. Post-request, `calculate_cost_cents(model, tokens_in, tokens_out)` computes the cost using current pricing. `deduct_credits` subtracts from credit pools in priority order (daily, bundled, signup bonus, purchased). BYOK requests log with `is_byok=True` and cost zero. ## BYOK (Bring Your Own Key) BYOK lets you use your own API keys instead of platform credits. Available on Pro and Ultra tiers. * Cost is zero in platform credits * No deduction from any credit pool * You pay the upstream provider directly on your own account * `UsageLog` is still created for your analytics, with `is_byok=True` and `billed_status="exempt"` * No platform margin on top Go to Settings, then the API Keys section. Pick a provider (OpenAI, Anthropic, OpenRouter, Google, or any supported provider), paste the key from the provider's console, optionally name it. The key is encrypted at rest with Fernet. Raw keys are never logged. Any chat that routes to a model matching this provider now runs BYOK. The model selector shows a BYOK badge. LiteLLM runs your BYOK keys in passthrough mode. Your key is used for the upstream call; the proxy handles routing, streaming, and cost accounting. OpenSail never proxies through its own key when a BYOK key is available for the model's provider. BYOK detection is provider-prefix based. The `is_byok_model(model_name)` check looks up the model's provider in `BUILTIN_PROVIDERS`. If you have a key stored for that provider, the call runs BYOK automatically. ## Self-hosted models For air-gapped deployments or latency-sensitive workloads, point LiteLLM at a local inference server. Ollama runs open-weight models on your hardware with a simple HTTP API. Add an entry to the LiteLLM config pointing at your Ollama host: ```yaml theme={null} model_list: - model_name: llama3-local litellm_params: model: ollama/llama3 api_base: http://ollama:11434 ``` The model `llama3-local` now appears in OpenSail's model selector and routes to your Ollama instance. vLLM is a high-throughput inference server with OpenAI-compatible endpoints. Configure LiteLLM to route to it: ```yaml theme={null} model_list: - model_name: qwen-coder litellm_params: model: openai/Qwen/Qwen2.5-Coder-32B-Instruct api_base: http://vllm:8000/v1 api_key: dummy ``` vLLM requires a dummy API key even for local access. LM Studio, Text Generation Inference, Fireworks, or any server exposing `/v1/chat/completions` works identically. Treat it as an `openai/`-prefixed provider with a custom `api_base`. Self-hosted inference requires enough VRAM for the model plus concurrent sessions. Plan for batch size and max sequence length. Ollama and vLLM both handle quantization for smaller GPUs. ## Model selector in chat Every chat session has a model selector at the top. The menu shows: * Models available to your subscription tier * BYOK-enabled models (if you have a matching key stored) * Self-hosted models (if your LiteLLM config includes them) * Cost badges: platform credits or BYOK or self-hosted Agents can also declare a default model as part of their configuration (see `/guides/customizing-agents`). Users can override per session unless the agent locks the model. ## Default models per tier | Tier | Typical default | Bundled credits (USD equivalent) | | ----- | ---------------------------------------------------------------- | -------------------------------- | | Free | Fast, low-cost (e.g., Haiku-class, GPT-3.5-class, Qwen-7B-class) | 0 bundled, 5 daily | | Basic | Mid-tier (Sonnet-class, GPT-4-class) | 500 | | Pro | Top-tier plus BYOK (Opus-class, GPT-4-class, or your own key) | 2,000 | | Ultra | Everything unlocked | 8,000 | Exact model availability is configured in LiteLLM's team settings. Your admin (or you, if self-hosting) can expose any subset of models to any tier. See `/guides/billing` for tier pricing details. ## Model parameters All LiteLLM-routed calls accept the standard OpenAI-compatible parameters: temperature, `max_tokens`, `top_p`, `frequency_penalty`, `presence_penalty`, and stop sequences. Agents can override per call; the platform does not impose hard caps beyond what upstream providers enforce. For extended-thinking capable models (Claude 3.7+, o1, o3), agents can pass a thinking budget. The default thinking effort is set via `default_thinking_effort` in the platform config. ## Cost management Use Haiku-class or Qwen-Coder-7B for UI tweaks and simple edits. Save Opus or GPT-4 for reasoning-heavy debugging and architecture. The cost delta is often 10x. The agent auto-compacts at 80% of the context window using a cheap summarization model (`compaction_summary_model`). Long runs accrue compaction cost; clearing chat between unrelated tasks saves more. If you run many hours per day, BYOK pays for itself quickly. No platform margin; you buy at provider list price. On Pro/Ultra with self-hosted Qwen-Coder or Llama 3.3, you can drive agents at near-zero marginal cost after GPU depreciation. Great for batch workflows and scheduled agents. ## Troubleshooting Your tier may not include that model. Check the model selector; greyed-out entries show the minimum tier required. For self-hosted, confirm the model is in the LiteLLM config and the inference server is reachable. The LiteLLM per-key budget is a \$10,000 runaway ceiling, not the real gate. If you hit it unexpectedly, your admin may need to run `ensure_budget_headroom()`. The real usage gate is your OpenSail credit balance. Switch to a faster model (Haiku, Flash, or a hosted Qwen-7B). Reduce `max_tokens`. Clear chat history to shrink input. If self-hosting, check GPU utilization; batched throughput dominates. Verify the key in the provider's console. Check it has sufficient balance. Some providers scope keys to specific models; confirm access. Rotate the key in OpenSail Settings. ## Next steps Set up BYOK model keys and external API keys for agent invocation Credit pools, tiers, and how BYOK changes the math Run OpenSail on your own infrastructure with local models Pick the right model mid-session # Projects Source: https://docs.tesslate.com/guides/projects A project is a working directory, a set of containers, a persistent volume, permissions, and a .tesslate/config.json graph. Everything you build on OpenSail lives inside one. Tesslate OpenSail ## What a project is A project on OpenSail is a self-contained working environment. It holds your source code, the containers that run it, a persistent volume that survives restarts, per-project permissions, and a typed graph of how everything connects. Every project carries: * A **slug** like `my-app-k3x8n2` that identifies it in URLs, logs, and the file system * A **runtime** (`local`, `docker`, or `k8s`) picked per project, not per workspace * A **visibility** flag (`team` or `private`) that governs who can open it * A **volume** that persists files across compute shutdowns * A **`.tesslate/config.json`** graph describing apps, infrastructure, connections, previews, and deploy targets * A **chat history** with every agent step ever run against it Your files, tracked and editable from Monaco, agents, and the terminal simultaneously. One or more services (frontend, backend, database) wired together in a shared network. btrfs-backed storage on Kubernetes, filesystem on Docker, host folder on desktop local. `.tesslate/config.json` is the source of truth for the Architecture Panel. ## Anatomy of a project A project has four pieces. All four are agent-readable and agent-writable. On Docker: `/projects/{user-id}/{project-id}`. On Kubernetes: the mounted btrfs subvolume in `proj-{uuid}`. On desktop: `$OPENSAIL_HOME/projects/{slug}-{uuid}` or a symlinked folder you adopted. The agent, the terminal, and the editor all see exactly this tree. Every container is a row in the `Container` table and a node on the Architecture Panel. Each has a name, image, port, env vars, a startup command, and a role (base, service, external, hybrid). Containers in the same project share a network and can reach each other by DNS. A project-scoped permission policy gates tool calls. Shell, network, git push, file writes, and process spawning each have an `allow`, `deny`, or `ask` policy. On desktop these live in `.tesslate/permissions.json`. On the cloud they combine with team RBAC (admin, editor, viewer) and project memberships. The canonical graph. Apps, infrastructure, connections, deployment targets, previews, and canvas positions all live here. The Librarian agent writes this during setup. Humans edit it on the Architecture Panel. Agents edit it with the `apply_setup_config` tool. Never edit this file through `write_file`. Always round-trip through the schema so Container records stay in sync. ## Runtime per project Every project picks its own runtime. The same desktop app can run one project locally with no containers while another runs in your production Kubernetes cluster. Subprocesses on the host machine. No Docker, no cluster, fastest cold start. Good for single-service prototypes and scripts. Uses `services/orchestration/local.py`. Docker Compose on your machine. Full container isolation, Traefik routing at `{container}.localhost`, shared volumes between services. Good for multi-service apps you still want to run locally. Full cloud runtime. Per-project namespace (`proj-{uuid}`), btrfs CSI volume, NGINX Ingress, VolumeSnapshot timeline, three-tier compute with hibernation. Good for anything you want to share, scale, or leave running while your laptop is closed. The runtime is a column on the `Project` model. `get_orchestrator(project=project)` dispatches to the right backend. You can switch runtimes, but it requires a restart and may involve a file sync. ## Three-tier compute Kubernetes projects live on a tiered model so idle workspaces cost almost nothing. | Tier | What runs here | Cost profile | | ---------- | --------------------------------------------------------------------- | -------------------------------- | | **Tier 0** | File reads, web fetches, agent reasoning, most tool calls | Near zero, no compute pod | | **Tier 1** | Shell commands via a warm ephemeral pool | Instant execute, returns to pool | | **Tier 2** | Full project namespace with live preview and long-running dev servers | On-demand, hibernates when idle | About 99 percent of agent operations stay in tiers 0 and 1. Tier 2 starts when you click Start or open a preview, and it hibernates (volume-level) after a configurable idle window. ## Visibility and access Projects are either `team` (inherits team RBAC) or `private` (owner-only). A team admin sees every team-visible project. A team editor can edit them. A team viewer can open them. Project-level overrides (`ProjectMembership`) let you grant or tighten access per user. Every significant action writes to an append-only audit log keyed by team and project. ## Lifecycle `POST /api/projects` creates the row, allocates a slug, and runs `_perform_project_setup` in the background. Setup clones the template or git repo, writes files to the volume, and syncs `.tesslate/config.json`. The Librarian agent inspects the tree and writes `.tesslate/config.json`. You can also hand-edit on the Architecture Panel or during the setup wizard. `POST /api/projects/{slug}/start` boots containers. On Kubernetes, it ensures the namespace, PVC, and file-manager pod, then creates Deployments, Services, and Ingress rules. Agents and humans edit files, run commands, and iterate. The preview hot-reloads. Steps persist progressively. After the idle window, the whole volume is snapshotted and compute torn down. Files are intact. Open the project and compute resumes from the snapshot. Same files, same containers. ## How agents see a project The agent receives a structured context on every run: project info, git status, the current `.tesslate/config.json`, open chat history, attached skills, and a live file tree. It writes through the same file API you use, so its edits and yours converge on the same checkout. When it changes infrastructure, it calls `apply_setup_config`, which validates startup commands, upserts Container rows, replaces connections, and writes the config file in one transaction. See the source at [orchestrator/app/routers/projects.py](https://github.com/TesslateAI/opensail/blob/main/orchestrator/app/routers/projects.py) for the full lifecycle API. ## Related Fork, snapshot, and share a project's full environment. Visual editor for `.tesslate/config.json`. Template, import, describe. How the agent runner drives projects. # Publishing Apps Source: https://docs.tesslate.com/guides/publishing-apps Turn an OpenSail workspace into an installable Tesslate App: write a manifest, walk the four-stage approval pipeline, configure billing, and distribute via bundles or forks. Tesslate OpenSail ## Overview A **Tesslate App** is a distributable package built on top of an OpenSail project. A creator develops a workspace normally, publishes it as a versioned `AppVersion`, and end users install it from the marketplace. Each install mints a fresh `Project(app_role="app_instance")` with its own Hub volume, containers, permissions, and billing relationship. The infrastructure for Apps sits on top of the same Project, Container, and Volume primitives you already use, with extra layers for versioning, approval, billing, and distribution. Each publish is an append-only `AppVersion` with a content-addressed bundle hash. Never mutated, always rollback-safe. Intake, automated scan, sandbox eval, and human review before a public install works. Three dimensions (AI compute, general compute, platform fee) with creator / platform / installer / BYOK payers. ## Concepts | Term | What it is | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Workspace | A regular `Project` you develop in. When you turn it into an app source it becomes `Project(app_role="app_source")`. | | AppVersion | An immutable, content-addressed snapshot (manifest + bundle hash). Published once, never mutated. | | MarketplaceApp | The public identity anchor: slug, handle, category, reputation, approval state. One app has many `AppVersion`s. | | AppInstance | A single user's installed copy. Installing mints a new `Project(app_role="app_instance")` with its own volume, containers, and permissions. | | `wallet_mix` | Per-install JSON declaring who pays for each billing dimension. Negotiated at install time. | | `update_policy` | `auto`, `manual`, or `pinned`. Controls whether new approved versions are pulled automatically. | | Surface | The entrypoint kind the app exposes: `ui` (iframe), `chat` (scoped agent session), `scheduled` (cron), `triggered` (webhook or event), `mcp-tool` (callable by other agents). A headless app declares `surfaces: []`. | Backing columns live in `orchestrator/app/models.py`. The full service map is in `docs/apps/CLAUDE.md`. ## Build your workspace Start from the dashboard or `POST /api/projects`. Use chat to describe what you want, or import code via the Desktop client's connected directories. Each container is a separate service (web, api, db, worker). The AI agent wires them through `setup-config` by reading `.tesslate/config.json`. The Librarian agent analyzes the project and generates `.tesslate/config.json` with `containers`, `startup_command`, and `connections`. Use `env_injection` edges so the manifest knows, for example, that the `api` container needs `DATABASE_URL` from the `db` container. See `seeds/apps/crm-with-postgres/app.manifest.json` for a full worked example. Start the project, hit the primary container URL, exercise schedules, verify secrets resolve. Set `app_role = "app_source"`. The Creator Studio UI does this on first publish. ## The app manifest The manifest is a JSON document describing everything a fresh installer needs: containers, surfaces, billing, scopes, schedules, MCP tool schemas. See the authoritative [2025-02 spec on GitHub](https://github.com/TesslateAI/opensail/blob/main/docs/specs/app-manifest-2025-02.md). The schema file is `orchestrator/app/services/apps/app_manifest_2025_02.schema.json`. ### Top-level shape ```yaml theme={null} manifest_schema_version: "2025-02" app: { id, name, slug, version, description, category, forkable, handle? } compatibility: { studio: { min, max? }, manifest_schema, runtime_api, required_features[] } surfaces: [ { kind, entrypoint?, name?, description?, tool_schema? } ] compute: tier compute_model containers: [ { name, image, primary, ports, env, startup_command, volumes?, resources? } ] connections: [ { source_container, target_container, connector_type, config } ] hosted_agents: [ ... ] state: { model, volume_size?, byo_database? } connectors: [ { id, kind, scopes, required, oauth?, secret_key? } ] schedules: [ { name, default_cron?, entrypoint?, execution, trigger_kind, editable?, optional? } ] billing: { ai_compute, general_compute, platform_fee, promotional_budget? } listing: { visibility, update_policy_default?, minimum_rollback_version? } eval_scenarios: [ ... ] ``` ### Minimal single-container app ```json theme={null} { "manifest_schema_version": "2025-02", "app": { "id": "com.tesslate.hello-node", "slug": "hello-node", "name": "Hello Node", "version": "0.1.0", "forkable": "true" }, "compatibility": { "studio": {"min": "0.0.0"}, "manifest_schema": "2025-02", "runtime_api": "^1.0", "required_features": [] }, "compute": { "tier": 1, "compute_model": "per-installer", "containers": [{ "name": "web", "primary": true, "image": "tesslate-devserver:latest", "ports": [3000], "startup_command": "node /app/server.js", "env": {"PORT": "3000"} }], "connections": [] }, "surfaces": [{"kind": "ui", "entrypoint": "/"}], "state": {"model": "per-install-volume", "volume_size": "256Mi"}, "billing": { "ai_compute": {"payer": "platform"}, "general_compute":{"payer": "platform"}, "platform_fee": {"model": "free"} }, "listing": {"visibility": "public"} } ``` ### Common fields * `slug` is kebab-case, globally unique, and immutable after first publish. * `version` is strict semver. Each publish must be strictly greater than the prior `AppVersion` for the same app. * `primary: true` marks the container whose URL backs `ui` / `chat` surfaces. Exactly one container must be primary when `containers[]` is non-empty. * Secret-ref env values use the convention `"${secret:/}"`. They resolve at pod-spec time via `env_resolver.py`. * `connectors[]` declares OAuth scopes the installer must consent to. * `eval_scenarios[]` supplies at least three happy-path prompts per entrypoint for public listings (used by Stage 2 sandbox eval). Headless apps (cron- or webhook-only) set `surfaces: []`. The workspace falls through to the Schedules tab as the primary layout. ## Submit for publishing From Creator Studio (`app/src/pages/CreatorStudioPage.tsx`) open your source project and click **Publish version**, or drive the REST API: ```http theme={null} POST /api/apps/versions/publish { "project_id": "", "manifest": { ... }, "app_id": null } ``` The publisher (`orchestrator/app/services/apps/publisher.py`) runs atomically: Manifest is validated against the frozen 2025-02 JSON Schema. `compatibility.check()` asserts the running server supports every declared `required_features` entry. By slug. First publish creates the public identity anchor. `(app_id, version)` must be unique. Bundle is written to Volume Hub's CAS and `bundle_hash` captured. `AppVersion(approval_state="pending_stage1")` and `AppSubmission(stage="stage0")` enter the review pipeline. Private or team installs (manifest `listing.visibility: "private"` or `"team:"`) still pass through Stage 0 and Stage 1 for structural checks, but never surface to the public marketplace. ## Approval pipeline Every public submission walks a four-stage state machine. Transitions are enforced by `orchestrator/app/services/apps/submissions.py` (`VALID_TRANSITIONS`). | Stage | What happens | Service | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | Stage 0 (intake) | Submission row created, bundle persisted. | `publisher.py` | | Stage 1 (automated scan) | Re-parses manifest, confirms declared features are supported, checks MCP scopes against the safe-list, confirms disclosure and billing payers. Hard fails reject; warnings do not. | `stage1_scanner.py` | | Stage 2 (sandbox eval) | Runs the app against an `AdversarialSuite` with a cheap model. Scores crashes, cost blowouts, prompt-injection resistance. Requires score >= `STAGE2_SCORE_THRESHOLD` (0.5). | `stage2_sandbox.py` | | Stage 3 (human review) | OpenSail admins sign off in the Admin Marketplace Workbench. | `admin_marketplace.py` | On Stage 3 approval, `AppVersion.approval_state` becomes `stage2_approved` and `MarketplaceApp.state` becomes `approved`. The version is installable from the public marketplace. Stage 1 writes one `SubmissionCheck` row per check, so creators can see exactly which check failed. Admins manage the adversarial suite itself from `AdminAdversarialSuitePage.tsx`. Dev shortcut `TSL_APPS_DEV_AUTO_APPROVE=1` skips all stages. The platform blocks this whenever `app_base_url` is HTTPS, so it cannot be enabled in production by accident. ## Billing configuration Billing has three independent dimensions. Each declares a payer and caps independently. The resolved payer for each spend event is the `wallet_mix` entry on the `AppInstance`, negotiated at install time. | Dimension | Payer options | Default `on_cap` | | ----------------- | -------------------------------------------- | ---------------- | | `ai_compute` | `creator`, `platform`, `installer`, `byok` | `pause` | | `general_compute` | `creator`, `platform`, `installer` (no BYOK) | `degrade` | | `platform_fee` | `subscription`, `one-time`, `free` | `pause` | ```yaml theme={null} billing: ai_compute: payer: installer cap_usd_per_session: 1.00 cap_usd_per_month_per_install: 50.00 general_compute: payer: installer cap_usd_per_month_per_install: 10.00 on_cap: degrade platform_fee: model: subscription price_usd: 9.00 billing_period: monthly trial_days: 7 promotional_budget: fund_usd: 500 covers: [ai_compute] on_exhaust: flip_to_installer ``` A `promotional_budget` lets the creator fund AI costs up to `fund_usd`. When exhausted, the payer automatically flips to the installer so the app keeps working. Settlement runs in ARQ via `settlement_worker.settle_spend_batch` with `SELECT ... FOR UPDATE SKIP LOCKED` for safe concurrency. The platform keeps `markup_pct` (default 10%) and credits the creator the rest. BYOK on `ai_compute` records a no-op `SpendRecord` with `reason='byok_no_op'`. Creators set billing from `app/src/pages/CreatorBillingPage.tsx`. Installers see the resolved numbers in `AppInstallWizard.tsx` before installing. ## Installing apps One-click install launches the `AppInstallWizard` (`app/src/components/apps/AppInstallWizard.tsx`). It collects: * Team under which to install. * OAuth consents for every `connectors[]` entry with `oauth: true`. * `wallet_mix` consent (which payer is accepted per dimension). * MCP scope consent per declared MCP server. * Update policy (`auto`, `manual`, `pinned`). On submit, `POST /api/apps/installs` reaches `installer.install_app()`, which: 1. Verifies the AppVersion is in `stage1_approved` or `stage2_approved`. 2. Re-runs compatibility against the current server. 3. Dedupes against an existing active `AppInstance`. 4. Calls `hub_client.restore_bundle()` to materialize a new volume from the CAS bundle. An `AppInstallAttempt(state="hub_created")` row is written before the DB commit so the background reaper (`install_reaper.py`) can clean up crashed installs. 5. Inserts a new `Project(app_role="app_instance")` plus one `Container` per entry in `compute.containers` and `ContainerConnection` rows for `compute.connections`. 6. Inserts the `AppInstance`, attaches `McpConsentRecord` rows, flips the attempt to `committed`. The installer returns `{app_instance_id, project_id, volume_id, node_name}`. Containers start through the normal orchestrator factory at `orchestrator/app/services/orchestration/factory.py`. Installed apps live in `app/src/pages/MyAppsPage.tsx`. The running UI is hosted by `app/src/components/apps/IframeAppHost.tsx` with a signed URL scoped to the install. ## Bundles Bundles group multiple AppVersions that should install together (for example the Tesslate Starter Pack: a CRM + nightly digest + center dashboard that embeds the others). * Model: `AppBundle` + `AppBundleItem`. * Service: `orchestrator/app/services/apps/bundles.py`. * Router: `orchestrator/app/routers/app_bundles.py`. * UI: `app/src/pages/BundleDetailPage.tsx` plus `app/src/components/apps/BundleInstallWizard.tsx`. `consolidated_manifest_hash` hashes member manifest hashes in sorted order, so semantically identical bundles deduplicate. The install wizard shows a single consolidated consent screen covering every member's OAuth scopes. The center-dashboard pattern uses `WorkspaceSurface.tsx` to render an app that embeds its bundle siblings via signed iframes. ## Forking If the manifest declares `forkable: "true"`, any user can clone the app: ```http theme={null} POST /api/apps/marketplace/{slug}/fork ``` `orchestrator/app/services/apps/fork.py` restores the source bundle to a new Hub volume, inserts a `MarketplaceApp(state="draft", forked_from=...)` row, and hands the forker a `Project(app_role="app_source")` they can edit and republish under their own slug. The marketplace surfaces fork lineage on `AppDetailPage.tsx`. | `forkable` value | Meaning | | ---------------- | ------------------------------------------ | | `"true"` | Anyone can fork | | `"restricted"` | Creator approval required per fork request | | `"no"` | Forks disabled | Forks do not inherit the source's consents or OAuth grants. The new owner re-wires hosted-agent tools and MCPs. ## Yanking If a version needs to be pulled, anyone with creator or admin rights can file a yank request via `orchestrator/app/services/apps/yanks.py` and `orchestrator/app/routers/app_yanks.py`. | Severity | Admin approvals | Typical reason | | ---------- | ------------------- | --------------------------------------------------------------------------------------------------------------- | | `low` | One admin | Cosmetic bug | | `medium` | One admin | Functional bug | | `critical` | Two distinct admins | Security or legal. Enforced by DB `CHECK ck_yank_critical_two_admin` and service-layer `NeedsSecondAdminError`. | Approved yanks flip `AppVersion.yanked_at` and `state = "yanked"`. Running installs stop being able to mint new runtime keys via `orchestrator/app/services/apps/runtime.py`, which refuses yanked and deprecated apps. Creators can file a `YankAppeal` against a decision. Admins review at `app/src/pages/AdminYankCenterPage.tsx`. ## Creator reputation `MarketplaceApp.reputation` is a JSON blob refreshed by `orchestrator/app/services/apps/monitoring.py` and its sweep worker `monitoring_sweep.py`. Inputs: * Star ratings and install count. * Review score from post-install feedback. * Uptime and crash metrics pulled from the compute tier. * Approval history (rejections, prior yanks, appeal outcomes). Aggregated creator reputation surfaces on the public creator profile (`orchestrator/app/routers/creators.py`) and in admin tooling at `app/src/pages/AdminCreatorReputationPage.tsx`. ## SDK usage For apps that call back into OpenSail (publish from CI, open a session against an install, invoke an MCP tool), use the versioned SDKs in `packages/tesslate-app-sdk/`. ```python Python theme={null} import asyncio from tesslate_app_sdk import AppClient, AppSdkOptions, ManifestBuilder async def main() -> None: opts = AppSdkOptions(base_url="https://opensail.tesslate.com", api_key="tsk_...") manifest = ( ManifestBuilder() .app(slug="hello", name="Hello App", version="0.1.0") .surface(kind="iframe", entry="index.html") .billing(model="wallet-mix", default_budget_usd=0.25) .require_features(["apps.v1"]) .build() ) async with AppClient(opts) as client: pub = await client.publish_version(project_id="...", manifest=manifest) inst = await client.install_app( app_version_id=pub["app_version_id"], team_id="...", wallet_mix_consent={"accepted": True}, mcp_consents=[], ) sess = await client.begin_session( app_instance_id=inst["app_instance_id"], budget_usd=1.0, ttl_seconds=3600 ) # sess["api_key"] is returned once only; cache it. asyncio.run(main()) ``` ```ts TypeScript theme={null} import { AppClient, ManifestBuilder } from "@tesslate/app-sdk"; const client = new AppClient({ baseUrl: "https://opensail.tesslate.com", apiKey: process.env.TESSLATE_API_KEY!, }); const manifest = new ManifestBuilder() .app({ slug: "hello", name: "Hello App", version: "0.1.0" }) .surface({ kind: "iframe", entry: "index.html" }) .billing({ model: "wallet-mix", default_budget_usd: 0.25 }) .requireFeatures(["apps.v1"]) .build(); const published = await client.publishVersion({ projectId: "...", manifest }); ``` Both SDKs authenticate with a Tesslate external API key (`tsk_...`) via `Authorization: Bearer`. CSRF is not needed because cookie sessions are not used. For the agent runtime that powers chat and hosted-agent surfaces, read the [tesslate-agent docs](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). ## MCP surface Any app can expose one or more of its entrypoints as MCP tools callable by other agents. ```yaml theme={null} surfaces: - kind: mcp-tool entrypoint: tools/redline name: redline description: "Return tracked-change suggestions on a document." tool_schema: type: object properties: document_ref: { type: string } required: [document_ref] ``` `tool_schema` is JSON Schema for the tool input. Stage 1 scans the declared MCP scopes; Stage 2 exercises the tool against the adversarial suite. At install time the user consents to scopes via `AppInstallWizard.tsx` and consent is recorded in `McpConsentRecord`. Runtime bridging lives in `orchestrator/app/services/mcp/` (client, bridge, manager). `stateless` or `shared-db` state models are recommended for MCP tools because they are called concurrently across invocations. ## Scheduled triggers and webhooks Every `schedules[]` entry gets an `AgentSchedule` row scoped to the install. Two dimensions combine into four shapes: | `trigger_kind` | `execution` | Semantics | | -------------- | ----------- | ------------------------------------------------------------------------------------- | | `cron` | `job` | Cron fires, a `V1Job` runs the `entrypoint` command in the primary container image. | | `cron` | `http-post` | Cron fires, POST to `${primary_url}${entrypoint}` with an invocation-key auth header. | | `webhook` | `job` | External POST to `/api/app-instances/{id}/trigger/{name}` kicks off a job. | | `webhook` | `http-post` | External POST is forwarded to the primary container endpoint. | Webhook authentication uses HMAC-SHA256 of the request body against `trigger_config.webhook_secret` in header `X-Tesslate-Signature`. Ingestion is fast: `schedule_triggers.ingest_trigger_event()` does a single INSERT into `ScheduleTriggerEvent`. `process_trigger_events_batch()` drains them into ARQ tasks using `SELECT ... FOR UPDATE SKIP LOCKED`. See `orchestrator/app/services/apps/schedule_triggers.py` and the routers `app_schedules.py` / `app_triggers.py`. Installers manage schedules from the My Apps UI; creators declare defaults and editability in the manifest. The `nightly_digest` seed at `seeds/apps/nightly_digest/app.manifest.json` is a full headless cron example (`surfaces: []`, one cron schedule). ## Admin review Tesslate staff drive the approval queue from these pages, all behind superuser auth on the router side (`orchestrator/app/routers/admin_marketplace.py`). | Page | Purpose | | ---------------------------------- | ------------------------------------------------------------ | | `AdminSubmissionWorkbenchPage.tsx` | Stage advance and per-check triage | | `AdminMarketplaceReviewPage.tsx` | Stage 3 final review | | `AdminYankCenterPage.tsx` | Yank requests, appeals, two-admin rule for critical severity | | `AdminAdversarialSuitePage.tsx` | Stage 2 eval suite management | | `AdminCreatorReputationPage.tsx` | Creator reputation overrides and audits | ## Where to next Ship a hosted build to Vercel, Netlify, Cloudflare, and 19 other providers. Put your installed app into Slack, Telegram, Discord, WhatsApp, or Signal. Distribute your app on the Tauri desktop shell. Need the agent authoring reference for hosted-agent surfaces? See the [tesslate-agent docs](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). Looking to trigger an installed app from outside OpenSail? See the external agent API at `orchestrator/app/routers/external_agent.py`. # Skills Source: https://docs.tesslate.com/guides/skills Teach an agent a reusable capability once and let any agent use it forever. Progressive loading keeps the context window lean. Tesslate OpenSail ## What a skill is A skill is a markdown document that teaches an agent a reusable capability. It has a name, a short description, and a body. The body can be anything that packages up working know-how: a code-review checklist, a research methodology, a data-analysis pipeline, a writing style, a report template, a step-by-step playbook for a recurring task. Skills are the unit of portable agent knowledge in OpenSail. Build a skill once, attach it to any agent, share it on the marketplace. Capture an entire methodology in a markdown file Attach to any agent on any project Only the name and description live in context; the body loads on demand Install open-source skills from the marketplace or publish your own ## Progressive loading Context windows are finite. Injecting every skill body at session start wastes tokens on capabilities the agent never uses. OpenSail solves this with progressive disclosure: When a chat session opens, the orchestrator discovers every skill available to the agent and injects a lightweight catalog into the system context: name, one-line description, source tag. Full bodies are not loaded. As the agent works, it scans the catalog for a relevant skill. When it finds one, it calls the `load_skill` tool with the skill slug. The orchestrator fetches the full body, applies any live marker substitutions (so built-in skills always match current platform behavior), and returns it to the agent as a tool result. The agent now has the full skill body in context and can apply it. The body stays in context for the rest of the session or until compaction. Progressive loading is automatic. Skill authors don't do anything special. Agents don't need special training. The `load_skill` tool and the catalog injection handle it transparently. ## The three skill sources Every agent session merges skills from three independent sources into one catalog: | Source | Where it lives | Who sees it | | ------------------- | ------------------------------------------------------------------------ | --------------------------------- | | Built-in | Seeded by the platform (`is_builtin=True`) | Every agent, every user | | Database assignment | `AgentSkillAssignment` rows created when you install a skill on an agent | Only agents you've attached it to | | Project file | `.agents/skills/SKILL.md` inside the workspace | Any agent working in that project | Duplicates are de-duplicated. If a user has both a built-in skill and an explicit assignment to the same skill, it appears once, tagged built-in. ## Browsing and installing skills Marketplace tab, then filter to Skills. Browse by category, tag, or creator. The detail page shows the skill body, the creator, the tags, and installation stats. Click Install. The skill lands in your Library with your ownership. Open the agent you want to extend. In the Skills tab, click Add Skill and pick from your Library. Start a chat session with that agent. The skill is in the catalog from the first turn. ## Authoring a skill Skills are plain markdown with a lightweight frontmatter contract. Stable kebab-case identifier. Example: `react-component-review`. This is what the agent reads to decide whether to load the body. Make it concrete. "Checklist for reviewing React components for accessibility and performance" beats "reviews code". Treat it like documentation for a junior engineer who needs to follow your methodology exactly. Include examples, anti-patterns, and edge cases. Markdown code fences and tables are fine. Open the Library, create a new skill, paste the body, save. Optionally submit it to the marketplace. ### What makes a good skill body "Run `npm test -- --coverage` and fail the review if coverage drops below 80%" beats "make sure there are enough tests". The agent executes top to bottom. Structure the body as a numbered or bulleted sequence the agent can walk through. Paste short code samples or CLI transcripts showing what correct output looks like. LLMs pattern-match examples better than abstract instructions. Spelling out what not to do is as valuable as what to do. "Never use `any` in TypeScript code" is a skill rule that saves turns. One skill per capability. If the body grows past a few hundred lines or covers multiple methodologies, split it into multiple skills. ## Tesslate skills vs open-source skills Shipped with the platform. Authored by the Tesslate team. Available to every agent without any attachment. Live-rendered: marker tokens like `{{TESSLATE_CONFIG_SCHEMA}}` or `{{SERVICE_CATALOG}}` are substituted with current values from the code, so skill bodies never drift from the actual platform behavior. Examples: * `project-architecture` (how OpenSail projects are structured) * Config schema skills that reflect the live `.tesslate/config.json` definition Authored by you or the community. Published under a license of your choice. Users browse, install, attach, fork. Open-source skills are editable after fork. Typical examples: * Code review checklists * Research methodologies * Writing style guides * Integration playbooks for specific APIs * QA testing scripts ## Project-file skills Any agent working in a project picks up skills from `.agents/skills/SKILL.md` automatically. This is how you ship project-specific know-how with the repo itself. ``` your-project/ ├── .agents/ │ └── skills/ │ ├── run-migrations.md │ ├── deployment-checklist.md │ └── review-pr.md ├── src/ └── package.json ``` Commit these files to git. Anyone working in the project with any agent gets the same skills. No install step required. ## Publishing a skill Use the skill on real work across multiple projects. Refine the body based on where the agent gets stuck or drifts. Open the skill in your Library and click Publish. Fill in the marketplace description, tags, and category. Private, team, or public. Public goes through the approval pipeline. Ship updates as new versions. Installers can pin a version or auto-update. ## Best practices The agent only knows the description. If your description is vague, your skill won't get loaded when it's needed. Make it specific and action-oriented. Long bodies burn context once loaded. Aim for the shortest body that fully captures the methodology. If multiple sub-methods are inside, consider splitting. A single agent can load many skills in one session. Prefer several small focused skills to one giant skill covering everything. Skills that work with one agent's system prompt may conflict with another's. If you publish a skill, test it against the default agents users will pair it with. ## Next steps Attach skills to your own custom agents Pair skills with connectors for end-to-end workflows Watch the load\_skill tool in action Browse published skills # Using Agents Source: https://docs.tesslate.com/guides/using-agents Start a session, watch tools run, approve dangerous operations, and drive a tesslate-agent instance to get work done Tesslate OpenSail ## What an agent is in OpenSail An agent in OpenSail is a running `tesslate-agent` instance, bound to a project workspace, that drives an LLM in a streaming tool-use loop. Every agent is a composition of four things: The personality, role, and rules of engagement. Co-authored by the creator. The built-in tool set (33 tools across 8 categories) plus any MCP connectors and view-scoped tools. Lightweight skill descriptions injected at session start. Full bodies fetched on demand. A LiteLLM-routed model for each session, with BYOK support and per-tier defaults. The active agent runner lives in the `packages/tesslate-agent` submodule and is invoked by the orchestrator worker through `tesslate_agent_adapter.py`. The agent is model-agnostic, stateless between calls, and streams every step back to the UI as it happens. For the authoritative, per-tool reference of all 33 built-in tools (parameters, edit-mode gating, return shape, quirks), see the tesslate-agent reference: [https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). ## Starting a session Open any project from your dashboard. The chat panel opens on the right and the agent picks up the project workspace, open files, and git state automatically. The agent selector at the top of the chat shows every agent installed on your account plus the project's default. Any agent published to the marketplace that you have installed is available here. The edit-mode toggle controls how much autonomy the agent has: Ask Before Edit (default), Allow All Edits, or Plan Mode. Mode is enforced at the tool registry level, not the system prompt, so every agent respects it. Type in natural language. Reference files by path. Drop in screenshots, error messages, or attached files. The agent reads the project tree, the git status, any `TESSLATE.md`, and the skill catalog before its first model call. Each tool call streams into the transcript as a separate card: the tool name, arguments, and result. Text the agent emits between tool calls streams inline. The session persists progressively to the database, so you can close the tab and come back later without losing the trajectory. ## The conversation pattern The agent runs an iteration loop until the model stops asking for tool calls: ``` loop: pre-flight compaction if context >= 80% of window model.chat_with_tools(messages, tools) if response text: stream it if no tool calls: emit complete and return run tool calls append tool results to history emit agent_step ``` You see streamed text, tool cards, and (on context pressure) a compaction marker. When the loop terminates with no more tool calls, you get a `complete` event and the chat is ready for your next message. ### Approval modes Dangerous tools (file writes, shell commands, web fetch) are gated at the registry level by the active edit mode: Default and safest. Reads proceed silently. Writes and shell commands pause the agent and show an approval card with Allow Once, Allow All, and Stop. Allow All for write tools flips the session into Allow All Edits; Allow All for shell/web tools persists per tool category only. Full autonomy. Every tool executes immediately. Use this once you trust the agent for a given task. Read-only. Writes, shell commands, and any mutating tool are blocked at the registry level. The agent is forced to produce a detailed plan as text rather than execute it. Switch into Plan Mode before risky migrations, refactors, or production work to dry-run the approach. Approvals persist only for the current chat session. Clearing the chat or starting a new one resets all Allow All decisions. ### Attaching files and context * Drag and drop files into the chat input to attach them. Images are viewable by the agent via `view_image` (supported on vision-capable models). * The architecture panel is shared state: nodes and edges you add show up to the agent on its next iteration, and the agent's graph edits show up on your canvas in real time. * Long-running sessions compact older messages automatically when the token estimate crosses 80% of the model's window. Multi-hour runs do not hit a wall. ## Running, interrupting, resuming Click Stop in the chat input while the agent is working. A cancellation signal is published to the worker, which checks between iterations and stops on the next loop boundary. Partial work is preserved in the transcript. Agent steps persist to the database as they happen. Close the tab, lose network, or have the worker pod restart: on reconnect the transcript rehydrates from the stored trajectory and the WebSocket resubscribes to the Redis event stream. Any events you missed replay before live streaming picks up. Every file write is recorded with a prior snapshot. Use `file_undo` to revert individual files, or use the workspace snapshot timeline to roll the entire project back to any point in the session. Up to 5 snapshots are retained per project. Agents work best when steered. After a tool card lands, you can interject with a short correction ("use Tailwind, not CSS modules") without waiting for the iteration to end. The correction lands in the next model turn. ## Writing prompts that work Include the file path, the framework, the expected behavior, and any constraints. "Create `src/components/Button.tsx` as a React TS component with `variant`, `size`, and `loading` props, using Tailwind" beats "add a button". Point the agent at a file, component, or pattern already in the project: "style this the same way `FeatureCard` is styled". The agent will read the referenced file and match the pattern. For multi-hour work, give the agent an ordered plan and let it tick off steps. Agents have planning tools (`update_plan`, `todo_write`, `save_plan`) and will use them to track progress. When debugging, paste the error output exactly. The agent pattern-matches stack traces faster than summaries and will search the codebase for the referenced symbols. ## Tool categories at a glance | Category | What the agent uses it for | | --------------- | ----------------------------------------------------------------------------------------------------- | | File operations | Read, write, patch, multi-edit, and undo files in the workspace | | Shell commands | One-off `bash_exec`, persistent `shell_open`/`shell_exec` sessions, background processes, Python REPL | | Navigation | `glob`, `grep`, and `list_dir` for fast codebase traversal | | Git | `git_log`, `git_blame`, `git_status`, `git_diff` inside the container | | Memory | Cross-session scratchpad the agent can read and write | | Web | `web_fetch`, `web_search` via Tavily, Brave, or DuckDuckGo | | Planning | Todos, plan documents, structured updates | | Delegation | Spawn subagents, wait on them, send messages, close them | For full parameter lists, gating rules, and return shapes, see the [tesslate-agent reference](https://github.com/TesslateAI/tesslate-agent/blob/main/docs/DOCS.md). ## Troubleshooting If you are in Ask Before Edit mode, the agent pauses for approval on the first dangerous tool. Check the chat for a pending approval card. You can also widen the approval scope with Allow All for the tool type. Plan Mode blocks writes and shell commands at the registry. Switch modes with the toggle next to the input. API-key scoping can also restrict tools: see `/guides/api-keys` for scope details. The loop terminates when the model stops asking for tools. If it keeps going, cancel, clarify the success criteria, and restart. Consider Plan Mode to force the agent to commit to an approach before executing. If you hit a credit ceiling mid-task, purchase a credit pack or upgrade your tier. See `/guides/billing`. BYOK keys (Pro and Ultra) bypass the credit system entirely. ## Next steps Build your own agent with a custom system prompt, skills, and MCP bindings Package a reusable capability and attach it to any agent Wire Slack, Gmail, Linear, and more into your agent's tool registry Pick the right model, BYOK, or self-host with Ollama or vLLM # Workspaces Source: https://docs.tesslate.com/guides/workspaces A workspace is the full, fork-able, snapshot-based environment that backs a project. Instant snapshots, 5-entry timeline, team-shareable, local-to-cloud portable. Tesslate OpenSail ## Workspace vs project A **project** is the logical entity: name, slug, containers, config. A **workspace** is the state those logical pieces produce on disk at any given moment: the files, the installed dependencies, the database rows, the running processes. Workspaces are built on btrfs, a snapshot-based filesystem. Everything you do in a project is captured in its workspace, and workspaces fork in seconds. Branch a running environment in seconds to try something risky. Up to 5 snapshots retained per project, plus one automatic timeline entry per major change. Share a workspace and the recipient gets code, state, config, and dependencies. Build on your laptop, push to your cluster, pull results back. ## Why btrfs OpenSail manages workspaces with a custom btrfs CSI driver plus a Volume Hub orchestrator (both in `services/btrfs-csi/`). Btrfs gives us: * **Copy-on-write subvolumes** so forking and snapshotting are near-instant * **Content-addressable sync to S3** for durable backup and cross-node transfer * **Per-node subvolume management** so compute can run wherever the volume cache lives * **Template clones** so new projects start from a warm, pre-warmed tree The orchestrator calls Volume Hub over gRPC: `CreateVolume`, `DeleteVolume`, `EnsureCached`, `TriggerSync`, `CreateServiceVolume`, `VolumeStatus`. Your project never touches btrfs directly, but this is why forks are instant and why idle projects cost almost nothing. ## Instant snapshots Every project maintains a rolling timeline. You get up to 5 `ProjectSnapshot` entries, each a K8s `VolumeSnapshot` plus a CAS reference. Snapshots fire automatically on major events (hibernation, app publish) and on demand from the Timeline panel. Volume Hub snapshots the btrfs subvolume and records the CAS address. Under 5 seconds in most cases. The oldest snapshot rolls off when you exceed 5. Retention is configurable via `K8S_MAX_SNAPSHOTS_PER_PROJECT`. Click any snapshot in the Timeline panel. The project restores from that exact subvolume state, containers and all. Snapshots are different from git commits. Snapshots capture the whole workspace (installed `node_modules`, database files, running state). Git captures source history. Use both. ## Forking a workspace Forking creates a new project with a new slug, a fresh volume cloned from the original, and copies of every Container, ContainerConnection, and BrowserPreview. The original keeps running; the fork starts in the stopped state. Common patterns: * **Experiment** Fork before a risky refactor. If it works, keep the fork. If it breaks, throw it away. * **Variants** Fork a working app to build a customer-specific version ("intake-base" becomes "intake-estate-planning"). * **Collaboration** Fork a teammate's workspace to contribute without touching their copy. The app marketplace uses the same primitive: install an app and you get a fork of its source workspace as a new project, with a `forked_from` provenance link. ## Sharing Because a workspace is self-contained, sharing one means sharing the full environment, not just a URL. Mark a project as team-visible and any team member with editor or admin rights can open it. Sharing uses the same volume and the same containers. Fork the workspace and transfer ownership. The recipient gets their own isolated copy with full edit rights. Bundle the workspace as a versioned app. Every install creates a new workspace with the same graph and source. ## Desktop to cloud The OpenSail desktop app and the OpenSail cloud orchestrator run the same server code. Pair your desktop to a cloud instance and a workspace can live on either side. Open a project on the desktop. It runs against SQLite and the local task queue. No network needed. Turn on sync. Files and config push to your paired cloud instance. The cloud side creates a mirror project. Switch runtime to `k8s` and the cloud takes over compute. Preview, multi-container, hibernation, all of it. Results sync back to your laptop. The workspace stays authoritative wherever you are working from most recently. The desktop is your home base; the cloud is your compute pool. Rebuilding one from the other takes seconds because the volume content is CAS-addressed in S3. ## Compute tiers and hibernation Workspaces on Kubernetes use a three-tier compute model. Most operations (file reads, web calls, reasoning) run on Tier 0 with no compute pod at all. Shell commands run on Tier 1 warm ephemeral pods. Long-lived dev servers and multi-container stacks run on Tier 2 pods that hibernate when idle. Hibernation is volume-level. When a Tier 2 project goes idle, the orchestrator triggers an S3 sync, tears down the compute pod, and leaves the volume cached on its node. Restoring just boots new compute against the same volume. ## Volume health Every project exposes a volume health signal. The `VolumeHealthBanner` surfaces cache node health, sync status, and pending migrations. If a cache node is unhealthy, Volume Hub peer-transfers the subvolume to another node automatically. You usually never notice. ## Related The logical entity a workspace backs. Edit the graph that defines the workspace topology. How btrfs CSI and Volume Hub deploy on your own cluster. Freeze a workspace into an immutable, installable bundle. # Welcome to OpenSail Source: https://docs.tesslate.com/introduction Build agents, apps, and automations. Run them on your infrastructure. Share them across your team. Tesslate OpenSail ## What is OpenSail? OpenSail is an open platform for building, running, and sharing AI-powered software. Describe a job you want done, drop in a file, and OpenSail helps turn it into a working agent, app, or workflow. Agents write code, use connected tools, remember what they learn, and continue work across many steps inside sandboxed cloud environments. You can use OpenSail on the cloud at [tesslate.com](https://tesslate.com), install it as a native desktop app, or self-host the whole stack on your own Kubernetes cluster. Anthropic, OpenAI, Google, Meta, Mistral, Qwen, DeepSeek, xAI, local Ollama, and more. Bring your own key or use ours. Fork a running workspace in seconds. Every project sits on a btrfs subvolume with instant snapshot, clone, and restore. Desktop, Docker, or Kubernetes. On-prem, air-gapped, or any cloud. No per-seat pricing, no lock-in. Turn any workspace into a one-click installable app. Share privately, with a team, or to the public marketplace. ## What you can build OpenSail treats every shipped unit as an "app." An app can be a UI, a chat interface, a scheduled cron job, a webhook handler, or an MCP tool callable by other agents. They are declared in the same manifest and built in the same workspace. Agents run in sandboxed containers, use connected tools (Slack, GitHub, databases, MCP servers), remember context across sessions, and continue work even when you close your laptop. Progressive persistence writes every step to the database as it happens, so you can resume mid-task. Multi-container projects with frontend, backend, database, and workers. React, Next.js, Node, Python, Go, any framework. Each project gets its own isolated namespace and volume. Agents that pull metrics every Friday, draft weekly reports, monitor support channels, route leads to CRM, or process files in a queue. Natural language schedules convert to cron expressions. Deploy agents to Slack, Telegram, Discord, WhatsApp, or Signal. Agents pick up requests, run work in sandboxed environments, and reply on the platform. Publish an agent as an MCP tool server so external coding agents (Claude Code, Cursor, Codex) can call it, get sandboxed compute, and use it as infrastructure. ## How it works Tell OpenSail what you want to build, or drop in a file. The agent plans the work, wires up the right tools, and asks clarifying questions. Monaco editor, a real terminal, a file tree, live preview with hot reload, git with branches and blame, a kanban board. The agent sees what you see. Every project runs in a three-tier compute model on Kubernetes. File ops and reasoning are near-free. Shell runs in warm pools. Full dev environments wake on demand and hibernate when idle. Draw an edge from a container to a deploy target. 22 providers supported: Vercel, AWS, Cloudflare Pages, Fly.io, and many more. Or publish as a one-click installable app. ## Technology | Layer | What | | ------------- | ---------------------------------------------------------------- | | Frontend | React 19, TypeScript, Vite, Tailwind, Monaco Editor, XYFlow | | Desktop shell | Tauri v2 (Rust), local SQLite, Stronghold token store | | Backend | FastAPI, Python 3.11, SQLAlchemy, LiteLLM | | Agent runner | `tesslate-agent` (Python), 33 built-in tools across 8 categories | | Database | PostgreSQL (cloud), SQLite (desktop) | | Task queue | Redis + ARQ (cloud), asyncio + apscheduler (desktop) | | Storage | btrfs CSI driver, Volume Hub orchestrator, S3-backed CAS | | Containers | Docker Compose (dev), Kubernetes (prod), subprocesses (desktop) | | Routing | Traefik (Docker), NGINX Ingress (Kubernetes) | ## Three ways to run Managed at tesslate.com. Sign up and build. Native Tauri app for macOS, Windows, Linux. Docker or Kubernetes on your own hardware. ## Why open source Workspace agents are powerful. They touch your data, your tools, your processes. You should be able to see exactly what they are doing, run them on your own infrastructure, and not be locked to a single model provider. OpenSail runs on any model, deploys on any cloud, and keeps your data where you put it. ## Next steps Build your first agent in five minutes. Clone, configure, boot. Twenty minutes from zero to running. macOS, Windows, Linux. No cluster needed. Turn a workspace into a one-click installable app. # Quickstart Source: https://docs.tesslate.com/quickstart Sign in, create a project, and ship your first OpenSail agent in five minutes Tesslate OpenSail This walkthrough uses the hosted cloud at [tesslate.com](https://tesslate.com). If you want to self-host, jump to the [Docker setup guide](/guides/docker-setup) or the [Desktop install guide](/guides/desktop-install) instead. ## Sign in Go to [tesslate.com](https://tesslate.com) and sign up with email or one of the OAuth providers (GitHub, Google). Check your inbox for a verification link. The first account on a fresh self-hosted install is promoted to admin automatically. The dashboard lists every project you own or have been given access to. A new account starts empty. ## Create your first project Pick a source: Start from a pre-wired base. Pick from Next.js, Vite+React+FastAPI, Vite+React+Go, Expo, and more. Paste a GitHub, GitLab, or Bitbucket URL. OpenSail clones the repo, detects the stack, and generates a `.tesslate/config.json` for the containers. Tell OpenSail what you want in plain English. An agent proposes the stack, writes the scaffolding, and checks in. Your project provisions a sandboxed container on a shared node. A loading overlay tracks progress; the container is usually ready in 10 to 30 seconds. The workspace layout has four panels: file tree, editor, live preview, and chat. A kanban board, git viewer, and architecture canvas are one click away. ## Build with the agent The chat panel is docked at the bottom. Expand it to full height for longer sessions. Be specific. Good prompts describe the behavior, not the code. For example: ``` Add a /api/contacts endpoint that returns a list of contacts from Postgres, ordered by last_interacted_at descending, paginated 20 per page. ``` The agent streams every step: file reads, edits, shell commands, test runs. You can interrupt, redirect, or roll back at any point. Files land on the project volume and the dev server reloads. The preview iframe updates without a refresh. ## Iterate and review Follow-up prompts build on the same context. The agent remembers decisions, reads files, and reasons about the whole project. Every edit is a reviewable diff. Accept the change, reject it, or edit it yourself in Monaco. Use the terminal panel for manual commands. The agent can also run commands itself when given permission. The git panel shows uncommitted changes. Write a message, commit, and optionally push to your remote. ## Ship it Three common next steps: Connect Vercel, AWS, Cloudflare, Fly, or another provider. Draw an edge from a container to a target. Set the agent to run on cron and deliver reports to Slack or Telegram. Turn the workspace into a one-click installable app on the marketplace. ## Learn more Projects, workspaces, agents, marketplace, the Architecture Panel. Conversation patterns, approval modes, skills, and connectors. Visual canvas that authors the `.tesslate/config.json` for your project. Ask questions, share what you are building. ## Tips Describe behavior, not syntax. Include: * What the feature does * Inputs and outputs * Edge cases you care about * Any constraints (libraries, style, performance) Ship something small that works, then expand. One prompt per feature beats one prompt for the whole app. OpenSail asks for approval before shell commands, git pushes, or network calls outside the allowlist. Review the diff, not just the chat summary. OpenSail autosaves, but git is still your history of record. Commit after each feature works. # Architecture Source: https://docs.tesslate.com/self-hosting/architecture How OpenSail is structured: orchestrator, agent runner, storage, three-tier compute, channels, and apps Tesslate OpenSail ## Overview OpenSail is an AI-powered platform for building, running, and sharing agents, full-stack apps, scheduled jobs, webhook handlers, and MCP tools. The system is modular: the same orchestrator drives desktop, Docker, and Kubernetes modes by swapping task queue, pub/sub, database, and container backends behind protocols. This page explains how the components fit together, how data flows at runtime, and how the security model is enforced. ## High-level diagram ```mermaid theme={null} flowchart TB D["Desktop (Tauri v2)"] --> API B["Browser UI"] --> API C["CLI / TUI"] --> API G["Gateway API + MCP Server"] --> API API{{"Orchestrator (FastAPI)"}} API --> Q["Task Queue
ARQ (cloud) or asyncio (desktop)"] Q --> W["Worker"] W --> AG["tesslate-agent runner"] API --> DB[("Database
Postgres or SQLite")] API --> R[("Redis Streams
pub/sub")] API --> ORCH{{"Orchestration Factory"}} ORCH --> DCK["DockerComposeOrchestrator"] ORCH --> K8S["KubernetesOrchestrator"] ORCH --> LOC["LocalOrchestrator (desktop)"] K8S --> VH["Volume Hub (gRPC)"] VH --> CSI["btrfs CSI driver"] CSI --> S3[("S3 CAS")] classDef surface fill:#eef2ff,stroke:#6366f1; classDef core fill:#ecfdf5,stroke:#10b981; classDef storage fill:#fff7ed,stroke:#f97316; class D,B,C,G surface; class API,Q,W,AG,ORCH,DCK,K8S,LOC core; class DB,R,VH,CSI,S3 storage; ``` ## Component layers | Module | Source | Notes | | ------------------ | -------------------------- | ------------------------------- | | App shell | `app/src/App.tsx` | React 19 router | | API client | `app/src/lib/api.ts` | SSE + fetch wrapper | | Editor | Monaco | code + diff view | | Chat | `app/src/components/chat/` | agent message stream | | Architecture Panel | React Flow | authors `.tesslate/config.json` | | Kanban | drag-and-drop board | | Tauri v2 Rust shell wraps a PyInstaller-frozen FastAPI sidecar. See `desktop/src-tauri/` and `desktop/sidecar/`. * Per-launch bearer token minted on startup * SQLite database under `OPENSAIL_HOME` * System tray keeps the sidecar alive when the window closes * Stronghold stores long-lived `tsk_` pairing keys * Deep-link handler for OAuth pairing callbacks | Module | Purpose | | ----------- | ---------------------------------------------------- | | `main.py` | Middleware, router mounting, CORS, CSRF | | `config.py` | Pydantic settings (env reader) | | `models.py` | 45+ SQLAlchemy models | | `routers/` | REST endpoints (projects, chat, billing, apps, etc.) | | `services/` | Business logic | | `agent/` | Legacy inline agent (still used in cloud path) | | `worker.py` | ARQ worker entrypoint | `packages/tesslate-agent` is the primary agent runner used by both desktop and cloud workers. It provides: * A tool registry (read/write, edit, bash, session, web search, fetch, skill loader, kanban, schedule, project control) * Progressive persistence: every `AgentStep` is written as it happens * Context compaction: older messages roll up via a cheap summarizer * Approval gates: tools can require human approval via permission policies * Cost and iteration caps (`AGENT_MAX_COST`, `AGENT_MAX_ITERATIONS`) ## Deployment modes | Mode | Database | Queue | Pub/sub | Containers | Best for | | ---------- | --------------------- | --------------------- | ------------- | -------------------------------- | ----------------- | | Desktop | SQLite (aiosqlite) | asyncio + apscheduler | in-process | subprocess / docker / remote k8s | Single user | | Docker | Postgres | ARQ on Redis | Redis Streams | Docker Compose | Dev, small teams | | Kubernetes | Postgres (cloud: RDS) | ARQ on Redis | Redis Streams | per-project namespaces | Multi-tenant prod | All three are selected by `DEPLOYMENT_MODE` and wired via factories in `orchestrator/app/services/`. ## Three-tier compute model The AI agent does not need a full Kubernetes pod every time it reads a file. OpenSail separates operations by cost: | Tier | What runs here | Backend | Wake time | | ------ | -------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------- | | Tier 0 | File ops, web calls, agent reasoning | In-process in the worker (FileOps gRPC to CSI) | Near zero | | Tier 1 | Shell commands | Warm ephemeral containers from a pool | Sub-second | | Tier 2 | Full dev environment: multi-container project, live preview, deploys | K8s namespace with multiple Deployments | On demand, hibernates when idle | About 99% of agent operations run on Tier 0 or Tier 1. Tier 2 is only needed when the user is actively working in the workspace or the agent needs to run the full stack. Hibernation is volume-level: a project snapshot captures the entire btrfs subvolume, then the namespace is torn down. Restore re-hydrates from CAS and brings all containers back together. ## Storage: btrfs CSI and Volume Hub User project data lives on btrfs subvolumes managed by a two-layer system. Location: `services/btrfs-csi/`. Runs as a DaemonSet. Responsibilities: * Create btrfs subvolumes (instant snapshot-clone from templates) * FileOps gRPC for agent Tier 0 file operations * NodeOps gRPC for template builds * S3 sync via CAS (content-addressed storage) * Per-node garbage collection Location: `services/btrfs-csi/pkg/volumehub/`. Single-pod storageless orchestrator. Responsibilities: * Track volume ownership and cache placement * Pick the best node for each project * Coordinate peer-transfer when a volume migrates nodes * Trigger S3 sync on hibernation * Expose gRPC (`CreateVolume`, `DeleteVolume`, `EnsureCached`, `TriggerSync`, `CreateServiceVolume`, `VolumeStatus`) Thin async client at `orchestrator/app/services/volume_manager.py` (calls hub via `hub_client.py`). The orchestrator never touches btrfs directly. Lifecycle: * Create: Hub picks a node, driver creates the subvolume * Compute: pod scheduled on `cache_node`, hostPath-mount * Hibernate: S3 sync triggered, pod removed, volume cached on node * Restore: `EnsureCached` tries fast local path, then peer-transfer, then S3 restore * Timeline: up to 5 K8s VolumeSnapshots per project ## Agent runner integration The orchestrator enqueues an `AgentTaskPayload` built from project state, chat history, git status, and `TESSLATE.md`. The ARQ worker picks up the task and runs the `tesslate-agent` loop. Redis-based distributed lock prevents concurrent runs on the same project. Each iteration: run the agent, persist `AgentStep` rows, publish events to the Redis Stream, check for a cancellation signal. The API router subscribes to the stream and forwards events over SSE or WebSocket. The client renders steps in real time. On completion: write the final Message, release the lock, optionally fire a webhook for external agent callers. Progressive persistence means pods can die, browsers can close, and the session is resumable. ## Apps subsystem An app on OpenSail is a versioned, immutable, manifest-described bundle produced from a workspace. Models: | Model | Role | | ---------------- | ------------------------------------------------------------------- | | `MarketplaceApp` | Identity anchor (slug, creator, category, state) | | `AppVersion` | Immutable version with manifest, CAS bundle address, approval state | | `AppInstance` | Per-user install with wallet mix and update policy | | `AppSubmission` | Staged approval pipeline row (stage0 → stage3 → approved) | | `YankRequest` | Unpublish flow; critical severity requires two admins | | `AppBundle` | Curated pack of AppVersions | Services: `installer.py`, `publisher.py`, `submissions.py`, `yanks.py`, `runtime.py`, `stage1_scanner.py`, `stage2_sandbox.py`. See the [Publishing Apps guide](/guides/publishing-apps). ## Channels and gateway Messaging integrations live under `orchestrator/app/services/channels/` with the Gateway v2 runner at `services/gateway/runner.py`. Platforms: Telegram, Slack, Discord, WhatsApp, Signal, CLI. Identity pairing links platform accounts to OpenSail users. Schedules (cron + timezone) deliver agent output to any configured channel. See [Communication gateways](/guides/communication-gateways). ## Security model | Layer | Enforcement | | ----------------------- | ------------------------------------------------------------------------------------------------------------- | | Transport | HTTPS in production; Traefik or NGINX Ingress | | Session | JWT with short-lived access + refresh rotation | | CSRF | Double-submit cookie with `X-CSRF-Token` header | | Passwords | bcrypt | | Secrets at rest | Fernet encryption via `SECRET_KEY`-derived key (`CHANNEL_ENCRYPTION_KEY`, `DEPLOYMENT_ENCRYPTION_KEY`) | | RBAC | `Team`, `TeamMembership` (admin/editor/viewer), `ProjectMembership` override, `AuditLog` | | Network isolation (K8s) | `NamespacePerProject` + `NetworkPolicy` enforcing zero cross-project traffic | | Agent capabilities | `.tesslate/permissions.json` gates shell, network, git push, file writes; approval prompts for `ask` policies | Every significant action writes to the append-only `AuditLog`, keyed by team and project. ## Next steps All environment variables with defaults. Path-by-path production guides. Request lifecycle, agent execution, container lifecycle. Three-tier compute, PVC lifecycle, snapshots. # Configuration Source: https://docs.tesslate.com/self-hosting/configuration Environment variable reference for OpenSail, grouped by subsystem, with defaults and examples Tesslate OpenSail ## Overview All OpenSail configuration is environment-driven. The canonical reader is `orchestrator/app/config.py` (Pydantic settings). Feature flags live in `orchestrator/app/config_features.py`. Frontend runtime config reads `window._env_.*`; build-time config reads `VITE_*`. Use this page together with: * Repo-root `.env.example` and `.env.prod.example` * `docs/guides/environment-variables.md` (the exhaustive reference) * Kustomize overlays under `k8s/overlays/*/backend-patch.yaml` After changing a variable, restart the affected service. In Docker: `docker compose restart orchestrator`. In Kubernetes: `kubectl rollout restart deploy/orchestrator -n tesslate`. ## Required core JWT signing and general crypto fallback. Never reuse across environments. Generate with `python3 -c "import secrets; print(secrets.token_urlsafe(32))"`. `docker`, `kubernetes`, or `desktop`. Selects orchestrator backend, DB driver, task queue, and pub/sub. SQLAlchemy async URL. Docker default points at the `postgres` service. Desktop mode resolves to SQLite automatically. Shared secret for cluster-internal callers (Volume Hub GC, btrfs CSI) hitting `/api/internal/*`. Desktop mode ignores. ## Database | Variable | Purpose | | ------------------- | -------------------------------- | | `POSTGRES_DB` | Database name | | `POSTGRES_USER` | DB user | | `POSTGRES_PASSWORD` | DB password | | `POSTGRES_PORT` | Host port (compose default 5432) | ```env theme={null} DATABASE_URL=postgresql+asyncpg://tesslate:tesslate@postgres:5432/tesslate POSTGRES_PASSWORD=dev_password_change_me ``` Desktop resolves a SQLite file under `OPENSAIL_HOME`. No Postgres variables required. ## Redis, pub/sub, task queue | Variable | Purpose | | -------------------- | ------------------------------------------------------------------------------------------ | | `REDIS_URL` | ARQ + RedisPubSub connection. Empty string falls back to in-memory for single-process dev. | | `WORKER_MAX_JOBS` | Concurrent agent tasks per ARQ worker pod (default 10) | | `WORKER_JOB_TIMEOUT` | Task timeout seconds (default 600) | Desktop mode uses `LocalTaskQueue` (asyncio + apscheduler) and `LocalPubSub` (in-process). No Redis required. ## LiteLLM and agent | Variable | Purpose | | -------------------------- | ----------------------------------------------------- | | `LITELLM_API_BASE` | Proxy URL, e.g. `https://your-litellm.example.com/v1` | | `LITELLM_MASTER_KEY` | Master key for minting per-user virtual keys | | `LITELLM_DEFAULT_MODELS` | Comma-separated models granted to new users | | `LITELLM_TEAM_ID` | LiteLLM team or access group | | `LITELLM_EMAIL_DOMAIN` | Internal email domain stamped into LiteLLM users | | `LITELLM_INITIAL_BUDGET` | Starting budget USD per user | | `COMPACTION_SUMMARY_MODEL` | Cheap model used for context compaction | | `DEFAULT_THINKING_EFFORT` | Extended thinking effort for supported models | | `AGENT_MAX_COST` | Global cost cap USD (default 20.0) | | `AGENT_MAX_ITERATIONS` | Global iteration cap, 0 disables | | `AGENT_MAX_COST_PER_RUN` | Per-run cost cap USD (default 5.0) | ## Auth and cookies JWT signing algorithm. Access token TTL. Refresh token TTL. Separate key for CSRF tokens. Falls back to `SECRET_KEY`. CSRF token TTL seconds. Set true in production. `lax`, `strict`, or `none`. Scope cookies to apex, e.g. `.tesslate.com`, for subdomain access. Comma-separated allowed origins. Host header allowlist. ## OAuth providers All optional. Absence of credentials disables the provider gracefully. | Provider | Variables | | -------- | ----------------------------------------------------------------------- | | GitHub | `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_OAUTH_REDIRECT_URI` | | Google | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_OAUTH_REDIRECT_URI` | | Provider | Variables | | ------------ | ----------------------------------------------------------------------------------------- | | Vercel | `VERCEL_CLIENT_ID`, `VERCEL_CLIENT_SECRET`, `VERCEL_OAUTH_REDIRECT_URI` | | Netlify | `NETLIFY_CLIENT_ID`, `NETLIFY_CLIENT_SECRET`, `NETLIFY_OAUTH_REDIRECT_URI` | | Heroku | `HEROKU_CLIENT_ID`, `HEROKU_CLIENT_SECRET`, `HEROKU_OAUTH_REDIRECT_URI` | | DigitalOcean | `DIGITALOCEAN_CLIENT_ID`, `DIGITALOCEAN_CLIENT_SECRET`, `DIGITALOCEAN_OAUTH_REDIRECT_URI` | Used when an MCP server does not advertise RFC 7591 Dynamic Client Registration. | Provider | Variables | | ------------------ | ---------------------------------------------------------------------- | | GitHub Copilot MCP | `MCP_OAUTH_APP_GITHUB_CLIENT_ID`, `MCP_OAUTH_APP_GITHUB_CLIENT_SECRET` | | Slack MCP | `MCP_OAUTH_APP_SLACK_CLIENT_ID`, `MCP_OAUTH_APP_SLACK_CLIENT_SECRET` | ## SMTP and 2FA | Variable | Purpose | | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_USE_TLS`, `SMTP_SENDER_EMAIL` | Email transport for magic links, 2FA codes, and password resets | | `TWO_FA_ENABLED` | Require 6-digit email 2FA after password login (default false) | ## S3 and object storage | Variable | Purpose | | ------------------------------------------ | ----------------------------------------------------------- | | `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | Credentials. Omit on AWS with IRSA. | | `S3_ENDPOINT_URL` | Custom endpoint for DO Spaces, MinIO, etc. Omit for AWS S3. | | `S3_BUCKET_NAME` | Projects bucket (default `tesslate-projects`) | | `S3_REGION` | Region (default `us-east-1`) | | `S3_PROJECTS_PREFIX` | Key prefix (default `projects`) | ## Kubernetes (user-project runtime) | Variable | Purpose | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `K8S_DEVSERVER_IMAGE` | Image for user containers. Minikube uses `tesslate-devserver:latest`; prod uses an ECR URL. | | `K8S_IMAGE_PULL_SECRET` | Registry pull secret (`ecr-credentials` in prod) | | `K8S_STORAGE_CLASS` | StorageClass for PVCs | | `K8S_SNAPSHOT_CLASS` | VolumeSnapshotClass | | `K8S_SNAPSHOT_RETENTION_DAYS` | Days to keep soft-deleted snapshots (default 30) | | `K8S_MAX_SNAPSHOTS_PER_PROJECT` | Max snapshots in timeline (default 5) | | `K8S_SNAPSHOT_READY_TIMEOUT_SECONDS` | Snapshot readiness timeout (default 300) | | `K8S_HIBERNATION_IDLE_MINUTES` | Auto-hibernate after N idle minutes (default 10) | | `K8S_HYDRATION_TIMEOUT_SECONDS`, `K8S_DEHYDRATION_TIMEOUT_SECONDS` | Hibernation transitions | | `K8S_PVC_SIZE` | Default PVC size per project (default 5Gi) | | `K8S_ENABLE_POD_AFFINITY` | Keep multi-container projects on the same node | | `K8S_RWX_STORAGE_CLASS` | ReadWriteMany class for shared source code | | `K8S_INGRESS_CLASS` | Ingress class name (default `nginx`) | | `K8S_NAMESPACE_PER_PROJECT` | Namespace-per-project isolation (default true) | | `K8S_ENABLE_NETWORK_POLICIES` | Create NetworkPolicies for project isolation (default true) | | `K8S_WILDCARD_TLS_SECRET` | TLS secret for wildcard cert (empty for HTTP) | | `K8S_INGRESS_DOMAIN` | Apex domain for user-project ingress | | `K8S_USE_S3_STORAGE` | Use S3 hibernation instead of PVCs (default false) | ## Volume Hub and btrfs CSI | Variable | Purpose | | --------------------------------- | ---------------------------------------------------------------------- | | `VOLUME_HUB_ADDRESS` | Hub gRPC endpoint (default `tesslate-volume-hub.kube-system.svc:9750`) | | `TEMPLATE_BUILD_STORAGE_CLASS` | StorageClass for template builds (`tesslate-btrfs`) | | `TEMPLATE_BUILD_NODEOPS_ADDRESS` | NodeOps gRPC endpoint for template builds | | `FILEOPS_ENABLED` | Use FileOps gRPC service for v2 file operations (default true) | | `FILEOPS_TIMEOUT` | FileOps RPC timeout seconds (default 30) | | `COMPUTE_MAX_CONCURRENT_PODS` | Max concurrent compute pods (default 5) | | `COMPUTE_POD_TIMEOUT` | Pod readiness timeout (default 600) | | `COMPUTE_REAPER_INTERVAL_SECONDS` | Orphan pod reaper interval (default 60) | | `COMPUTE_REAPER_MAX_AGE_SECONDS` | Max pod age before reaping (default 900) | ## Web search | Variable | Purpose | | ---------------------- | --------------------------------------------------- | | `WEB_SEARCH_PROVIDER` | `tavily`, `brave`, or `duckduckgo` (default tavily) | | `TAVILY_API_KEY` | Tavily API key | | `BRAVE_SEARCH_API_KEY` | Brave API key | ## Messaging channels and gateway | Variable | Purpose | | ------------------------------ | ------------------------------------------------------ | | `AGENT_DISCORD_WEBHOOK_URL` | Webhook for the agent `send_message` tool | | `CHANNEL_ENCRYPTION_KEY` | Fernet key for encrypting per-user channel credentials | | `GATEWAY_ENABLED` | Enable gateway runner (default false) | | `GATEWAY_SHARD` | Shard identifier for multi-instance gateway | | `GATEWAY_TICK_INTERVAL` | Scheduler tick interval seconds | | `GATEWAY_SESSION_IDLE_MINUTES` | Idle timeout for gateway sessions | | `GATEWAY_VOICE_TRANSCRIPTION` | Enable voice message transcription | ## MCP (Model Context Protocol) | Variable | Purpose | | -------------------------- | -------------------------------------------------- | | `MCP_TOOL_CACHE_TTL` | Tool schema cache TTL seconds (default 300) | | `MCP_TOOL_TIMEOUT` | Tool call timeout seconds (default 30) | | `MCP_MAX_SERVERS_PER_USER` | Cap on installed MCP servers per user (default 20) | ## Stripe and billing | Variable | Purpose | | ---------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET` | Stripe core keys | | `STRIPE_CONNECT_CLIENT_ID` | Stripe Connect (creator payouts) | | `STRIPE_BASIC_PRICE_ID`, `STRIPE_PRO_PRICE_ID`, `STRIPE_ULTRA_PRICE_ID` | Monthly subscription prices | | `STRIPE_BASIC_ANNUAL_PRICE_ID`, `STRIPE_PRO_ANNUAL_PRICE_ID`, `STRIPE_ULTRA_ANNUAL_PRICE_ID` | Annual prices | | `CREDIT_PACKAGE_SMALL`, `CREDIT_PACKAGE_MEDIUM`, `CREDIT_PACKAGE_LARGE`, `CREDIT_PACKAGE_TEAM` | One-time credit prices (cents) | | `SIGNUP_BONUS_CREDITS`, `SIGNUP_BONUS_EXPIRY_DAYS` | Signup bonus | | `CREATOR_REVENUE_SHARE`, `PLATFORM_REVENUE_SHARE` | Revenue split decimals (sum to 1) | | `USAGE_INVOICE_DAY` | Day of month to generate usage invoices (1..28) | ## Frontend | Variable | Mode | Purpose | | ------------------------------------------------------- | ------- | --------------------------------------------------------------- | | `VITE_API_URL` | build | API base URL (empty for Vite proxy, or `http://localhost:8000`) | | `VITE_ALLOWED_HOSTS` | build | Comma-separated host allowlist for Vite dev server | | `VITE_PUBLIC_POSTHOG_KEY`, `VITE_PUBLIC_POSTHOG_HOST` | build | PostHog project | | `window._env_.API_URL` | runtime | Base URL without `/api` (frontend code prepends it) | | `window._env_.POSTHOG_KEY`, `window._env_.POSTHOG_HOST` | runtime | PostHog runtime config | ## Domain, ports, Traefik | Variable | Purpose | | ----------------------------- | -------------------------------------------- | | `APP_DOMAIN` | Apex domain (no protocol) | | `APP_PROTOCOL` | `http` or `https` | | `APP_PORT`, `APP_SECURE_PORT` | Public HTTP and HTTPS ports | | `BACKEND_PORT` | Uvicorn port | | `FRONTEND_PORT` | Vite dev port | | `APP_BASE_URL` | Full base URL; defaults to protocol + domain | | `TRAEFIK_DASHBOARD_PORT` | Dashboard port (dev) | | `TRAEFIK_BASIC_AUTH` | htpasswd admin credential | | `TRAEFIK_CERT_RESOLVER` | `letsencrypt` or `cloudflare` | | `CF_DNS_API_TOKEN` | Cloudflare DNS token for wildcard certs | ## Desktop | Variable | Purpose | | ---------------------- | ------------------------------------------------------------------------------------------ | | `OPENSAIL_HOME` | Desktop data directory override. Resolved by `orchestrator/app/services/desktop_paths.py`. | | `TEST_HELPERS_ENABLED` | Expose test-only routes (CI) | | `BUILD_SHA` | Deployment identifier reported by `/api/version` | ## Observability | Variable | Purpose | | ------------------------- | ---------------------------------------------------------------- | | `LOG_LEVEL` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` (default `INFO`) | | `VITE_PUBLIC_POSTHOG_KEY` | PostHog project key (empty to disable) | ## Feature flags (Tesslate Apps) Apps features are gated by `TSL_FEATURE_*` booleans. Default is off except governance policies. The env variable is the dotted flag name upper-cased with dots replaced by underscores: `apps.publish` becomes `TSL_FEATURE_APPS_PUBLISH`. | Flag | Default | | ------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `apps.manifest_schema_v1`, `apps.publish`, `apps.install` | false | | `apps.runtime.ui`, `apps.runtime.chat`, `apps.runtime.scheduled`, `apps.runtime.triggered`, `apps.runtime.mcp_tool` | false | | `apps.hosted_agent`, `apps.source_view`, `apps.fork`, `apps.bundles` | false | | `apps.review.stage1`, `apps.review.stage2`, `apps.review.stage3`, `apps.yank` | false | | `apps.yank.critical_two_admin` | true (governance policy) | | `apps.billing.dispatcher`, `apps.billing.revenue_split` | false | | `apps.triggers.webhook`, `apps.triggers.mcp_event`, `apps.triggers.app_invocation` | false | | `apps.canvas.hosted_agent_node`, `apps.embedding.postmessage` | false | ## Testing | Variable | Purpose | | ---------------------- | ----------------------------- | | `TEST_HELPERS_ENABLED` | Expose test-only routes in CI | | `PLAYWRIGHT_BASE_URL` | Base URL for Playwright E2E | CI sets `SECRET_KEY=test-secret-key-*`, `DEPLOYMENT_MODE=docker`, `LITELLM_API_BASE=http://localhost:4000/v1`, `LITELLM_MASTER_KEY=test-key`, and points `DATABASE_URL` at Postgres on port 5433. ## Environment comparison | Setting | Docker (Local) | Minikube | AWS EKS | | ------------------------- | -------------- | --------------------------- | ------------------------------------- | | `DEPLOYMENT_MODE` | `docker` | `kubernetes` | `kubernetes` | | `K8S_DEVSERVER_IMAGE` | N/A | `tesslate-devserver:latest` | `/tesslate-devserver:latest` | | `K8S_IMAGE_PULL_SECRET` | N/A | (empty) | `ecr-credentials` | | `K8S_STORAGE_CLASS` | N/A | `tesslate-btrfs` | `tesslate-block-storage` | | `K8S_WILDCARD_TLS_SECRET` | N/A | (empty, HTTP) | `tesslate-wildcard-tls` | | `APP_DOMAIN` | `localhost` | `localhost` | e.g. `opensail.example.com` | | `COOKIE_SECURE` | `false` | `false` | `true` | ## Next steps Production paths: Docker, Kubernetes, AWS EKS. How the orchestrator, agent, and storage fit together. JWT, OAuth, email 2FA, API keys, and desktop pairing. REST surface overview and OpenAPI spec pointer. # Deployment Source: https://docs.tesslate.com/self-hosting/deployment Overview of the three production paths for OpenSail: Docker Compose, Kubernetes on Minikube, and AWS EKS with Terraform Tesslate OpenSail ## Overview OpenSail has three supported deployment paths. This page summarizes what each path is good for and links to the detailed walkthroughs. For the install-path decision matrix, see the [self-hosting quickstart](/self-hosting/quickstart). Prefer a managed service? [tesslate.com](https://tesslate.com) runs OpenSail for you with zero setup. ## Pick your path Single-server install with Postgres, Redis, Traefik, the orchestrator, and the app in Compose. Best for dev laptops and single-VM deployments. Full Kubernetes pipeline on your own machine: per-project namespaces, btrfs CSI, Volume Hub, snapshots. The staging ground before production. Production deployment via Terraform. EKS, ECR, managed Postgres, S3-backed CAS, wildcard TLS, and Cloudflare DNS. ## What each path gives you | Capability | Docker | Minikube | AWS EKS | | --------------------------------- | :---------------: | :--------: | :-----------------------: | | Orchestrator + agent + app | Yes | Yes | Yes | | Postgres | In-cluster | In-cluster | RDS | | Redis | In-cluster | In-cluster | ElastiCache | | LiteLLM gateway | External or local | External | External | | Per-project namespaces | No | Yes | Yes | | NetworkPolicy isolation | No | Yes | Yes | | btrfs CSI + Volume Hub | No | Yes | Yes | | CAS bundles on S3 | No | MinIO | S3 | | Snapshot timeline (5 per project) | No | Yes | Yes | | Hibernation with restore | No | Yes | Yes | | Wildcard TLS | Traefik + LE | HTTP only | cert-manager + Cloudflare | | Horizontal scaling | No | Demo only | Yes | ## Overview of each path The Compose stack brings up every service on a single host: | Service | Purpose | | ---------------- | --------------------------------------------------- | | `orchestrator` | FastAPI backend | | `frontend` (dev) | Vite + React | | `postgres` | Database | | `redis` | Task queue + pub/sub | | `traefik` | `*.localhost` routing (dev) or Let's Encrypt (prod) | | `worker` | ARQ agent worker | `DEPLOYMENT_MODE=docker` selects `DockerComposeOrchestrator`, which provisions per-project containers from `.tesslate/config.json` and wires them into the Traefik network. Start here: [Docker setup guide](/guides/docker-setup). Minikube mirrors the production stack end to end so you can validate the K8s path without cloud cost. | Component | Source | | ---------------------- | -------------------------------- | | Base manifests | `k8s/base/` | | Minikube overlay | `k8s/overlays/minikube/` | | btrfs CSI + Volume Hub | `services/btrfs-csi/` | | Namespaces | one per project (`proj-{uuid}`) | | Storage | `tesslate-btrfs` StorageClass | | Snapshots | `tesslate-btrfs-snapshots` class | Apply with `kubectl apply -k k8s/overlays/minikube` then pair env with `k8s/.env.minikube`. Everything runs HTTP. Start here: [Kubernetes on Minikube](/guides/kubernetes-local). Terraform provisions the platform and environment stacks under `k8s/terraform/aws/` and `k8s/terraform/shared/`. Kustomize applies to the resulting EKS cluster. | Layer | Resource | | ------------ | ------------------------------------------------------------------------------------- | | Networking | VPC, private subnets, NAT, VPN | | Cluster | EKS 1.29 with managed nodegroups (btrfs-capable) | | Registry | ECR for `tesslate-orchestrator`, `tesslate-frontend`, `tesslate-devserver` | | Database | RDS Postgres | | Cache | ElastiCache Redis | | Object store | S3 bucket for CAS + snapshots | | Ingress | NGINX Ingress + cert-manager + Cloudflare DNS | | Secrets | AWS-managed; auto-mounted via `envFrom` in `k8s/overlays/aws-base/backend-patch.yaml` | Start here: [AWS production deployment](/guides/aws-production). ## Shared operational surface Regardless of path: * The orchestrator reads config from environment variables. See [Configuration](/self-hosting/configuration) for the full reference. * Agents run via the `tesslate-agent` package (`packages/tesslate-agent/`), either inline (desktop, Docker) or under an ARQ worker (K8s). * Project volumes are provisioned through `orchestrator/app/services/volume_manager.py`, which talks to the Volume Hub on Kubernetes or the local filesystem on Docker and desktop. * Authentication and RBAC are consistent across paths. See [Authentication](/authentication). ## Next steps Fastest path to a running OpenSail. Full K8s locally to validate the production path. Terraform, EKS, RDS, S3, wildcard TLS. Env variable reference. # Self-Hosting Quickstart Source: https://docs.tesslate.com/self-hosting/quickstart Pick the right path to self-host OpenSail: Docker, Desktop, Kubernetes on Minikube, or AWS EKS Tesslate OpenSail ## Overview OpenSail is Apache 2.0 and self-hosts cleanly. There are four supported install paths. This page is a router. Pick the path that fits your goal, then follow the dedicated guide. Prefer a managed experience? [tesslate.com](https://tesslate.com) runs OpenSail as a hosted service with zero setup. ## Pick your path One command brings up the full stack on your laptop or a single VM. The fastest way to kick the tires. Best for local development and small teams. Native Tauri v2 app with a built-in FastAPI sidecar and SQLite. No Docker, no cluster. Runs fully offline against local models. Full K8s pipeline on your machine: per-project namespaces, btrfs CSI volumes, Volume Hub, snapshots. Test the production path end to end. Production deployment via Terraform. EKS, ECR, managed Postgres, S3-backed CAS, wildcard TLS via cert-manager. ## Decision matrix | You want to... | Pick | | ---------------------------------------------------- | ----------------- | | Kick the tires in 10 minutes | Docker Compose | | Build on a single machine with no containers | Desktop | | Air-gapped install with Ollama | Desktop or Docker | | Serve a small team on one VM | Docker Compose | | Multi-tenant production with isolation and snapshots | AWS EKS | | Validate the K8s pipeline before prod | Minikube | | Pair a local app to your own self-hosted cloud | Desktop + AWS EKS | | Capability | Docker | Desktop | Minikube | AWS EKS | | --------------------------------------- | :-----: | :------------: | :------: | :-----: | | AI agent runs (`tesslate-agent`) | Yes | Yes | Yes | Yes | | Live preview and Monaco editor | Yes | Yes | Yes | Yes | | Multi-container projects | Yes | docker runtime | Yes | Yes | | Per-project namespace isolation | No | No | Yes | Yes | | btrfs CSI and Volume Hub | No | No | Yes | Yes | | Snapshot timeline (up to 5 per project) | No | No | Yes | Yes | | Hibernation with CAS restore | No | No | Yes | Yes | | Three-tier compute (0 / 1 / 2) | Partial | Partial | Yes | Yes | | Wildcard TLS via cert-manager | No | No | No | Yes | | SQLite (no Postgres) | No | Yes | No | No | | No Redis required | No | Yes | No | No | | Horizontal scaling | No | No | No | Yes | | Path | Setup | Moving parts | Ongoing ops | | -------------- | --------- | ------------------------------------------------------------ | --------------------------------- | | Docker Compose | 10 min | Postgres, Redis, Traefik, orchestrator, app, devserver image | Restart, rotate backups | | Desktop | 2 min | Tauri binary, sidecar, SQLite | Installer auto-updates | | Minikube | 30 min | Cluster, kustomize, local images, btrfs CSI, Volume Hub | Rebuilds, manifest rollouts | | AWS EKS | 2 to 4 hr | Terraform, EKS, ECR, RDS, S3, Cloudflare, wildcard TLS | Cluster upgrades, cost monitoring | ## Prerequisites by path | Requirement | Notes | | ------------------------------- | ------------------------------------------------------------ | | Docker Engine + Compose v2 | Docker Desktop on macOS and Windows | | 8 GB RAM minimum | 16 GB recommended | | 10 GB free disk | For images and volumes | | A model key or LiteLLM endpoint | BYOK OpenAI, Anthropic, OpenRouter, or point at local Ollama | | Git | For cloning the repo | | Requirement | Notes | | -------------------------------- | --------------------------------------------- | | macOS 12+, Windows 10+, or Linux | Tauri v2 builds for all three | | 4 GB RAM minimum | SQLite and sidecar are lightweight | | 2 GB free disk | Grows with projects | | A model key | BYOK or Ollama for fully offline use | | Optional: Docker | Enables the `docker` per-project runtime | | Optional: `tsk_` API key | Unlocks marketplace, sync, remote K8s runtime | | Requirement | Notes | | ------------------------------- | ---------------------------------- | | Minikube | With the Docker or KVM driver | | kubectl + kustomize | Kustomize ships with kubectl 1.14+ | | btrfs-capable node | Required for the btrfs CSI driver | | Local Docker registry | Minikube loads images by name | | A model key or LiteLLM endpoint | BYOK or proxy | | Requirement | Notes | | ----------------------- | -------------------------------------- | | AWS account with admin | EKS, ECR, RDS, S3, IAM | | Terraform | `k8s/terraform/aws/` | | Cloudflare DNS | For apex and wildcard certs | | Managed Postgres | RDS recommended | | ElastiCache Redis | For ARQ and pub/sub | | btrfs-capable nodegroup | Required for CSI driver and Volume Hub | | LiteLLM deployment | Or direct provider keys on the proxy | ## Core environment variables Every path reads the same core variables. The full reference lives in [Configuration](/self-hosting/configuration). | Variable | Why it matters | | ----------------------------------------- | ------------------------------------------------------------------------------------- | | `SECRET_KEY` | JWT signing and Fernet encryption for stored credentials | | `DEPLOYMENT_MODE` | `docker`, `kubernetes`, or `desktop`. Selects orchestrator, DB driver, queue, pub/sub | | `DATABASE_URL` | Postgres (cloud) or resolved SQLite (desktop) | | `LITELLM_API_BASE` + `LITELLM_MASTER_KEY` | AI gateway that fans out to providers | | `APP_DOMAIN` | Apex domain for routing, cookies, and OAuth redirects | | `OPENSAIL_HOME` | Desktop only. Data directory for `opensail.db`, projects, and cache | Generate a secure `SECRET_KEY`: ```bash theme={null} python3 -c "import secrets; print(secrets.token_urlsafe(32))" ``` ## What you get after install | Component | Source path | Purpose | | ----------------- | --------------------------------------------- | ----------------------------------------------------------------------- | | Orchestrator | `orchestrator/app/main.py` | FastAPI backend, router registration, middleware | | Agent runner | `packages/tesslate-agent/` | Agent loop, tool registry, context compaction, approval gates | | Task queue | `orchestrator/app/services/task_queue/` | ARQ on Redis (cloud) or asyncio + apscheduler (desktop) | | Pub/sub | `orchestrator/app/services/pubsub/` | Redis Streams (cloud) or in-process (desktop) | | Orchestration | `orchestrator/app/services/orchestration/` | Per-project `factory.py` resolves Docker, Kubernetes, or local backends | | Volume Hub client | `orchestrator/app/services/volume_manager.py` | gRPC client to the Hub (K8s only) | | Frontend | `app/src/App.tsx` | React 19, Vite, Monaco | ## Next steps Full Docker Compose install with seeds and clean-slate reset. Installers, cloud pairing, permission system, runtime selection. End-to-end K8s locally including btrfs CSI and Volume Hub. Terraform, EKS, ECR, wildcard TLS, S3 CAS, managed Postgres. Every environment variable, grouped by subsystem. Orchestrator, agent runner, storage, queue, compute tiers. ## Getting help Source, releases, and issue tracker. Ask questions, share what you are building. Deep dive on the `tesslate-agent` package. Direct support.