# API keys (/account/api-keys) API keys authenticate scripts, SDK clients, and other server-side automation. Each key belongs to your account and one workspace, with a read-only or read-and-write scope. See [API authentication](/api/authentication) for request headers and [API overview](/api/overview) for the supported API surface. ## Create a key [#create-a-key] 1. Open **Account → API Keys** in the dashboard. 2. Select **Create API key**. 3. Give it a specific name, such as `production-deploy-ci`. 4. Select the workspace and choose read-only or read-and-write access. 5. Choose an expiry and create the key. 6. Copy the complete key immediately into your secrets manager. The full key is shown once. The list later displays a prefix, scope, expiry, and last-used time, not the secret value. If you lose the key, create a replacement and revoke the old one. Keys can have an expiry from 1 to 365 days. Choose the shortest practical lifetime and schedule rotation in your own operational process. ## Understand scope and role [#understand-scope-and-role] | Scope | Intended use | | ------------ | ---------------------------------------------------- | | Read only | Read supported workspace resources | | Read & write | Read and change supported resources within your role | A read-and-write key does not make its creator an admin. Protected environments and operation-specific roles still apply. Viewers can create only read-only keys. Access also depends on current account status, verified email, workspace membership, and any workspace two-factor requirement. Revoked or expired access cannot be bypassed with a previously created key. ## Store keys securely [#store-keys-securely] Use your CI system's secret store or a server-side environment variable such as `OPENSTEAD_API_KEY`. Keep keys out of source control, browser bundles, analytics, logs, and issue descriptions. Create different keys for unrelated automation. A descriptive name and narrow scope make revocation easier when a workflow or integration is retired. API keys are for the supported API. Browser-only payment actions and phpMyAdmin handoffs require an authenticated dashboard session. ## Rotate or revoke [#rotate-or-revoke] To rotate, create a replacement, update the consuming application or CI secret, verify the new key works, and revoke the old key. Keep any overlap as short as practical. Use the revoke action beside the key in **API Keys**. Requests using it immediately lose access. Remove the old value from the external secret store as well. ## Troubleshooting [#troubleshooting] An authentication error can indicate a missing, malformed, expired, or revoked key. A permission error can indicate insufficient scope, a role restriction, a protected environment, or a workspace security requirement. Check the structured API error before creating new credentials. Avoid repeated retries for a permanent permission failure. When asking support for help, share the key's display name or prefix and request ID, never the complete key. # Projects and environments (/account/projects) Projects organise related services inside a workspace. Environments organise a project's runtime resources and help separate production from development or staging. ## Create a project [#create-a-project] From the workspace's project view, create a project with a name, optional description, and colour. Openstead creates a **Production** environment automatically. A project is an organisational container, not a compute instance. Its services have their own plans and deployment history. ## Find projects and services [#find-projects-and-services] The workspace **Overview** shows a card for each project, with its name, description, and a summary of its services' status. Select a project to open it. Services that do not belong to a project appear in the **Ungrouped services** table below the project cards. Inside a project, each environment has its own section and service table. Select a service name to open its details, or use an environment's **New service** button to create a service in that project and environment. Environment groups scoped to an environment are linked below its service table. Status summaries follow the same [health checks](/deployments/health-checks) used elsewhere in the console; an old successful deployment does not make an unavailable service appear live. Use a service's [environment-variable settings](/deployments/environment-variables) to manage shared variables and [database connection references](/databases/connections). ## Add environments [#add-environments] Create a Staging environment when you need to test code with separate configuration or data. A workspace includes up to two environments per project without qualifying paid services, or twenty with a qualifying active paid service. An example layout: ```text Storefront project Production storefront-web storefront-worker storefront-db Staging staging-web staging-worker staging-db ``` The staging database is a separate service. Adding an environment does not clone production data automatically. ## Private networking and references [#private-networking-and-references] Applications and managed databases need to be in the same environment for private connection references. Choose the project and environment when creating each service. If you move a service to a different environment, review its database references and linked environment groups. A reference to a source in the old environment becomes invalid. Prepare the destination dependencies before deploying the moved application. See [Private networking](/networking/private-networking) for network scope and [Database connections](/databases/connections) for reference behaviour. ## Protect an environment [#protect-an-environment] Owners and admins can mark an environment as protected. Protected environments restrict changes to owners and admins, including operations on their services and related configuration. Use this for production resources when developers should have less direct write access. Protection follows shared variables, database connection references, and preview copies. A service outside the protected environment that inherits this access keeps its owner/admin restriction after a link is removed. Running and retained deployments can still contain those credentials, so unlinking, moving the service, or deploying again does not automatically lower its access requirements. Owners and admins can continue managing it. Environment protection is a role boundary. It is not an application test, a deployment-approval workflow, or a substitute for safe database migrations. ## Share configuration carefully [#share-configuration-carefully] Environment groups let you reuse configuration across linked services. Scope a group to an environment when those values should not cross into another environment, and keep production credentials out of staging groups. Service-level variables and connection references still need to be consistent. A connected-database variable cannot also be defined with the same key in an ordinary variable or linked group. See [Environment variables](/deployments/environment-variables). ## Rename and remove [#rename-and-remove] Renaming a project or environment updates its label; confirm any scripts that locate resources by name still select the intended item. Prefer resource IDs in API automation. Move or delete services before deleting their environment or project. Linked environment groups also need to be moved or removed. These guards help prevent accidental deletion of containers that still own resources. # Account security (/account/security) Open **Account & security** in the dashboard to manage your password, connected sign-in providers, two-factor authentication, and active sessions. ## Sign-in methods [#sign-in-methods] Use email and password or an enabled social provider shown on the sign-in screen. The **Last used** label under a social button is a convenience hint for that browser. It does not authenticate you or select an account automatically. To add another social sign-in method to an existing account, first sign in using a method already connected to it, then connect the provider from your account security settings. Matching email addresses do not automatically merge unrelated accounts. GitHub sign-in is separate from authorising the GitHub App to deploy repositories. See [GitHub deployments](/deployments/github). ## Email verification [#email-verification] Verify your account email before accessing workspace operations. Under **Account Settings → Email addresses**, you can add an address, request another verification email, and make a verified address primary. Check spelling and spam folders if mail does not arrive. Invitation acceptance requires a verified email matching the invitation, so verify the invited address rather than trying a different account. ## Password recovery [#password-recovery] Use **Reset it** on the login page if you cannot remember your password. Open the newest recovery email and complete the reset flow. Do not share a reset link with another person. Security changes may require you to confirm your identity again. This protects sensitive actions even when a browser has an existing session. ## Enable two-factor authentication [#enable-two-factor-authentication] 1. Open **Account & security** and start authenticator setup. 2. Scan the QR code with a TOTP authenticator app, or use the manual setup key. 3. Enter a current code to confirm setup. 4. Save the generated recovery codes somewhere secure and separate from the device. Use a recovery code if the authenticator is unavailable. Each recovery code is intended for one use. Treat the setup key and recovery codes like passwords; do not include them in screenshots or support messages. If codes are rejected, check the device's clock and time synchronisation, then enter a fresh code. Contact [support](https://openstead.tech/contact) if you have lost every valid recovery method; do not repeatedly create new accounts to try to recover a workspace. ## Workspace enforcement [#workspace-enforcement] Workspace owners and admins with the relevant paid entitlement can require TOTP for members. Enable two-factor authentication on your own account before setting that requirement. The requirement applies when accessing workspace resources, including API operations tied to a member. It does not replace the member's workspace role or API-key scope. ## Review active sessions [#review-active-sessions] Use the sessions list to inspect current sign-ins and revoke devices you no longer use. Sign out when finished on a shared device. Revoking the browser session also ends dependent phpMyAdmin access on subsequent requests. Merely closing a browser tab is not the same as revoking a session. ## Protect application credentials [#protect-application-credentials] Store application secrets in [environment variables or secret files](/deployments/environment-variables), and create separate [API keys](/account/api-keys) for separate automation purposes. Never put an Openstead API key into browser JavaScript. If a secret is exposed, revoke or rotate it at its source and update the applications that use it. Removing a leaked value from Git's latest commit does not remove it from earlier history. # Team members and roles (/account/team-members) Workspace roles control what a member can see and change. Invite each teammate with their own account rather than sharing a login or API key. ## Available roles [#available-roles] | Role | Typical access | | --------- | -------------------------------------------------------------------------------------------------------- | | Viewer | View workspace resources and activity without configuration changes | | Developer | Create and update application resources and perform permitted deployment operations | | Admin | Developer access plus billing, team administration, backup management, and protected-environment changes | | Owner | Admin access plus ownership transfer, owner-only member controls, and workspace deletion | Individual operations can require a higher role. For example, database-backup management requires admin access, and a protected environment restricts changes to admins and owners. A write-scoped API key cannot exceed its creator's role. ## Invite a teammate [#invite-a-teammate] 1. Open **Workspace settings → Team Members**. 2. Select **Invite members**. 3. Enter the teammate's email and choose the permitted role. 4. Send the invitation. 5. Ask the recipient to sign in and verify the matching email address before accepting. Invitations expire after three days. A pending invitation reserves a member slot until it expires or is revoked. Resend an expired invitation from the dashboard instead of forwarding an obsolete link. A workspace with a qualifying paid service includes up to ten members. A Free workspace includes one member. See [Workspaces](/account/workspaces). ## Who can invite or change roles [#who-can-invite-or-change-roles] Owners and admins can invite developers and viewers. Only an owner can invite an admin or promote someone to admin. An admin cannot remove or change another admin or the owner. There is no owner role in an ordinary invitation. To change ownership, invite the person, wait for them to become a member, and use the explicit ownership-transfer action. ## Remove access [#remove-access] Remove a member in Team Members when they no longer need access. Confirm the correct email address before removal. Membership checks apply to subsequent authorised requests. Also review credentials that may have been copied outside Openstead, such as third-party API tokens or manually copied database passwords. Removing membership cannot erase a secret already stored elsewhere. Revoke unused invitations to prevent later acceptance. Invitations are tied to the matching verified email and cannot be used to overwrite an existing member's role. ## Transfer ownership [#transfer-ownership] The current owner selects another existing workspace member and confirms their email address. The selected member becomes owner; the previous owner becomes an admin. Confirm the recipient is the intended person and has working account recovery and two-factor authentication before transferring a production workspace. The owner must transfer ownership before leaving the workspace. ## Require two-factor authentication [#require-two-factor-authentication] Qualifying paid workspaces can require members to configure TOTP two-factor authentication. The person enabling the requirement must first enable it on their own account. Communicate the change to the team and ask members to save recovery codes. See [Account security](/account/security). # Workspaces (/account/workspaces) A workspace is the top-level home for a team's Openstead resources. It contains projects, services, environment groups, member permissions, integrations, and billing records. Your personal account can belong to multiple workspaces. Each workspace has its own membership and resource access; signing in does not grant access to every workspace. ## Select or create a workspace [#select-or-create-a-workspace] Use the workspace switcher in the dashboard to choose where you want to work. Create a separate workspace when a team or client needs independent membership, billing, or integration access. Use [projects and environments](/account/projects) to organise related applications inside one workspace. Do not create extra workspaces merely to model staging and production for the same team. ## Workspace settings [#workspace-settings] Open **Workspace settings** to manage the name, notification preferences, team members, build policy, registries, security settings, and activity exports available to your plan. The workspace ID shown there is useful for API requests and support. It identifies the workspace but does not authenticate access. Owners and admins can update workspace settings. Use [team roles](/account/team-members) to grant narrower access to people who only need deployment or viewing capabilities. ## Included access [#included-access] | Capability | Without qualifying paid services | With a qualifying active paid service | | ---------------------------------------------- | -------------------------------- | --------------------------------------------------- | | Workspace members | 1 | Up to 10 | | Environments per project | Up to 2 | Up to 20 | | Shared build minutes | At least 500 per UTC month | Greater of 500 or pooled paid-service contributions | | Private registry configuration | Not included | Included | | Audit CSV export and workspace MFA enforcement | Not included | Included | A paid service qualifies through its actual purchased, deployed access. A draft plan selection, suspended instance, or expired term is not equivalent to an active paid service. See [Billing](/billing/overview). Personal two-factor authentication and baseline workspace access checks apply independently of these paid workspace benefits. ## Notifications and build policy [#notifications-and-build-policy] Choose a verified workspace member's email for operational notifications and keep it monitored. Account and workspace notification preferences can control which deployment events produce messages. Review overlapping-deployment behaviour and the shared pipeline allowance before enabling automatic deploys on many services. One concurrent build is included; editing customer preferences does not create extra infrastructure capacity. See [Builds](/deployments/builds). ## Activity and audit history [#activity-and-audit-history] Workspace activity records changes such as service updates, member changes, and integration actions. Owners and admins with the audit-export entitlement can download a CSV for a selected date range of up to one year. Use activity history to understand who changed a resource. It complements application logs, which describe the application's own runtime behaviour. ## Leave or delete a workspace [#leave-or-delete-a-workspace] Members can leave a workspace. The owner must transfer ownership before leaving. Deleting a workspace requires the owner, an explicit name confirmation, and removal of its service configurations first. Do not use workspace deletion as a shortcut for suspending a paid application. Export required data, resolve billing records, and review every service before deleting resources. # Authentication (/api/authentication) Openstead API keys authorize access to one workspace. Keep them in your server environment or CI secret store. ## Create a key [#create-a-key] 1. Sign in to the [Openstead dashboard](https://openstead-dashboard.vercel.app/dashboard). 2. Open **Account settings → API Keys → Create API key**. 3. Enter a descriptive name, select the workspace, choose the scope, and choose an expiry. 4. Copy the displayed key into a secret store. The complete value is shown when created. Use a separate key for each integration so you can revoke it without interrupting unrelated systems. ## Send the key [#send-the-key] ```bash curl --fail-with-body \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ "https://api.openstead.tech/api/v1/catalog" ``` Keys begin with `rnv_`. Pass the exact `Bearer` scheme and key in the header. Do not put a key in a URL, commit it to Git, or embed it in a frontend bundle. The TypeScript SDK belongs in server code, including a Next.js Route Handler or server-only module. ## Scopes and roles [#scopes-and-roles] | Scope | Access | | -------------- | ----------------------------------------------------- | | Read only | Read resources available to your workspace role. | | Read and write | Read and mutate resources within your workspace role. | Scope does not grant a higher role. Developers can manage ordinary services and request deployments. Project deletion and changes to services in protected environments require an admin or owner. Current membership, account status, verified email, and any workspace MFA policy are checked on requests, including idempotent replays. A key from one workspace cannot read a different workspace. There is no workspace-list or workspace-create operation in the public core contract; use the workspace ID associated with your key. ## Paid service operations [#paid-service-operations] Create a paid service configuration with `deploy: false`, complete its checkout in the dashboard, then deploy it with your API key. A key can manage deployments within an authorized service term. It cannot purchase a term, charge a payment method, or provide payment consent. ## Rotate or revoke access [#rotate-or-revoke-access] Create a replacement key, update your secret store, verify a read request, and revoke the old key from **API Keys**. Expired or revoked keys stop working. Removing the key owner's membership also removes their access. The [CLI](/integrations/cli) offers browser-assisted sign-in and stores its credential in the operating system keychain. It uses the same workspace permissions. ## Diagnose authentication failures [#diagnose-authentication-failures] * `401 invalid_api_key`: verify the value, expiry, and whether it was revoked. * `403 insufficient_scope`: check both the key's scope and workspace ID. * `403 email_verification_required`: verify your account email. * `403 mfa_required`: enable the MFA required by the workspace. * `403 permission_denied`: ask a workspace owner to review your role or the environment's protection. Browser session cookies are for dashboard interactions. Use bearer keys for SDK, CI, and server integrations; a validated bearer key does not require a CSRF token. # API compatibility (/api/compatibility) The public core uses the `/api/v1` prefix and is defined by the [OpenAPI 3.1 specification](https://api.openstead.tech/api/v1/openapi.json). Its `info.version` records the contract revision. The current revision is `1.0.0`. ## Stable interfaces [#stable-interfaces] Documented operation IDs identify operations consistently for generated clients. Breaking changes to a documented request requirement, response field type, authorization behavior, or resource meaning require a new major contract and migration path. Additional endpoints, optional request fields, and additive response data can appear within the same major version. ## Write forward-compatible clients [#write-forward-compatible-clients] * Ignore response fields your application does not use. * Tolerate unknown future status strings without treating them as success. * Read the catalog for current choices and capabilities. * Use UUIDs from responses, not resource names, in REST paths. * Inspect envelopes and asynchronous state instead of assuming every 200 response means completed work. * Use documented cursor and retry rules rather than inferring them from another endpoint. Both official SDKs preserve additional response fields and tolerate future response status values. Polling helpers retain a deadline for unknown states. ## Service configuration [#service-configuration] Create and update accept the configuration fields defined in the specification. Unknown configuration keys are rejected. Constraints also depend on the service kind, plan, and current workspace entitlements. PATCH merges supplied configuration values with existing ones. Omitting a field and explicitly clearing a nullable field are different operations. A service's kind cannot change in place. ## Contract boundaries [#contract-boundaries] The public core covers catalog, workspace reads, projects, services, variables, deployments, and logs. The dashboard and CLI may expose additional workflows with different contracts. An endpoint's presence in a browser network trace does not make it part of the supported core SDK interface. SDK releases carry a copy of the specification used for their types. Pin an SDK version in your dependency lockfile, review release notes when upgrading, and validate the behavior your integration relies on. ## Report an incompatibility [#report-an-incompatibility] Contact [Openstead support](https://openstead.tech/contact) with the endpoint, HTTP method, returned request ID, SDK version, and a redacted example. Include the expected and observed behavior without credentials, secret values, or customer records. # Deploy an application with the API (/api/deployments) This walkthrough assumes a connected GitHub repository, a workspace UUID, and a write-scoped key. Replace the example repository with one your workspace can access. ## Create service configuration [#create-service-configuration] Read `GET /catalog` to choose an available plan and runtime. This example saves a free Python web service without deploying it: ```json { "name": "example-api", "kind": "web", "configuration": { "sourceType": "repository", "repository": "https://github.com/example/example-api", "branch": "main", "runtime": "python", "buildMethod": "railpack", "rootDirectory": "backend", "plan": "free", "port": 8000, "startCommand": "gunicorn config.wsgi:application --bind 0.0.0.0:8000" }, "deploy": false } ``` Save the body as `service.json`. Ensure `backend/config/wsgi.py` exists and Gunicorn is included in your application's dependencies, or adjust the path and start command. ```bash curl --fail-with-body --request POST \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: configure-example-api-001" \ --data-binary @service.json \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/services" ``` Store the returned `service.id` as `OPENSTEAD_SERVICE_ID`. To place it in a project, provide both `projectId` and a compatible `environmentId`; project creation returns its Production environment. Omitting both creates an ungrouped service. For a paid plan, save configuration first and complete checkout in the dashboard before deploying. API credentials cannot authorize a purchase. ## Queue a deployment [#queue-a-deployment] ```bash curl --fail-with-body --request POST \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: deploy-example-api-001" \ --data '{}' \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/services/$OPENSTEAD_SERVICE_ID/deploys" ``` The empty body deploys the configured branch. To select a commit, provide `commitSha` as a full lowercase hexadecimal SHA of 40–64 characters. Source access and deployment permissions still apply. Record `deployment.id` from the response. Reuse the same idempotency key after an uncertain response; a new key requests another deployment. ## Observe progress [#observe-progress] Read `GET /workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}` until it reaches a terminal state. | State | Meaning | | ------------------------------------ | ------------------------------------------------------------------ | | `queued` | Waiting for execution; inspect `blockedReason` if present. | | `preparing`, `cloning`, `building` | Preparing source and producing the release. | | `predeploy`, `deploying`, `checking` | Running release preparation and checking the deployed application. | | `live` | Deployment succeeded. | | `failed` | Deployment failed; inspect its error and logs. | | `cancelled` | Cancellation completed. | | `superseded` | Another release replaced this deployment request. | The last four states are terminal. SDK wait helpers return on `live` and raise for unsuccessful terminal states. Read the service to inspect its public `url`, serving `status`, and `liveDeploymentId`. A new deployment can be building while an earlier release continues serving traffic. ## Updates, cancellation, and rollback [#updates-cancellation-and-rollback] PATCH merges supplied configuration fields; omitted fields retain their values. The service kind is immutable. Saving configuration does not itself create a deployment. Cancellation requests return `cancelRequested: true`; continue observing until the deployment reaches a terminal state. Rollback creates a **new deployment** from a retained successful release, using that release's saved configuration and secret snapshot. Supply the exact service name in `confirm`, then poll the new returned deployment ID. Review application/database compatibility before rolling back code. ## Runtime operations [#runtime-operations] `POST .../services/{serviceId}/actions` with `{"action":"restart"}`, `suspend`, or `resume` returns a queued operation. Poll `GET .../operations/{operationId}` until `complete` or `failed`. The `deploy` and `clear-cache` actions return a deployment instead. Archive prevents new deployments by setting a configuration flag. It does not suspend infrastructure. Restore clears that flag without deploying. # Errors and request IDs (/api/errors) API errors include a readable message and a stable machine-readable `code`. Handle the code rather than matching the message text. ```json { "errors": [ { "message": "This idempotency key was already used with different request values.", "code": "idempotency_conflict" } ], "requestId": "client-trace-01" } ``` An error entry may also include `field` to identify an invalid input. ## HTTP status codes [#http-status-codes] | Status | Meaning | Next step | | ------ | --------------------------------------------------- | ----------------------------------------------------------- | | 400 | Invalid JSON, values, pagination, or operation | Correct the request using its error code. | | 401 | Missing, invalid, or expired authentication | Check or replace the credential. | | 403 | Scope, role, email, MFA, or entitlement restriction | Resolve the named access requirement. | | 404 | The resource is unavailable to this caller | Check the UUID and workspace. | | 405 | Unsupported HTTP method | Use the method shown in the reference. | | 409 | Resource state, uniqueness, or idempotency conflict | Read existing state before submitting another write. | | 413 | Request is too large | Keep the JSON body at or below 256 KiB. | | 429 | Request throttled | Honor `Retry-After` before retrying. | | 5xx | A server-side failure | Retry reads or supported keyed writes with bounded backoff. | The API provides a `Retry-After` header on 429 responses. Do not assume a fixed allowance of requests per minute; use the returned delay. ## Correlate a request [#correlate-a-request] All API responses include `X-Request-ID`. Error bodies repeat it as `requestId`. You may supply a trace value using `X-Request-ID`: 1–64 letters, digits, dots, underscores, or hyphens, beginning with a letter or digit. Invalid values are replaced by a generated identifier. Record the returned value with the timestamp and endpoint when contacting [support](https://openstead.tech/contact). `X-Request-ID` identifies a request for diagnostics. The legacy write-body field also named `requestId` is an alias for **Idempotency-Key** and has different semantics. See [idempotency](/api/idempotency). ## Deployment failures [#deployment-failures] A successful HTTP request can return a deployment whose current state is `failed`. The transport succeeded; the deployment did not. Check `status`, review its build or runtime logs, and inspect the returned deployment's safe error details. A local timeout while polling means your observation window ended. It does not cancel the remote job or prove it failed. Resume with the existing deployment or operation ID. ## Safe diagnostics [#safe-diagnostics] Store status, error code, request ID, resource IDs, and SDK version where appropriate. Avoid recording complete request bodies, bearer credentials, revealed variables, or unrestricted application logs. SDK exception messages omit sensitive response contents by default; explicitly accessed error entries still require careful handling. # Idempotency and retries (/api/idempotency) An idempotency key identifies one logical write. If a response is lost, an identical retry with the same key can return the original result without performing the operation again. ## Send a stable key [#send-a-stable-key] ```bash curl --fail-with-body \ --request POST \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: create-example-project-001" \ --data '{"name":"example-project"}' \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/projects" ``` Use 1–160 visible ASCII characters without spaces. Persist the key alongside your operation data before sending a request when the workflow can restart. The legacy JSON-body `requestId` field is also accepted. If both forms are supplied, they must match. Prefer the header for new REST integrations. ## Supported public writes [#supported-public-writes] * Project create, update, and delete. * Service create, update, delete, archive, restore, and actions. * Service-variable create, update, and delete. * Deployment create, cancel, and rollback. Secret reveal is excluded. Do not attach a key to reveal or assume another dashboard endpoint supports replay. Unsupported operations reject the header with `idempotency_not_supported`. ## The 24-hour window [#the-24-hour-window] Successful keyed responses are retained for **24 hours**. The key is scoped to the actor, credential, method, and request path within the workspace. Retries must use the same request values, query parameters, and key. | Response | Meaning | | ----------------------------- | ------------------------------------------------------- | | `Idempotency-Replayed: false` | This keyed operation was committed now. | | `Idempotency-Replayed: true` | This is the retained successful response. | | `409 idempotency_conflict` | The key was already used with different request values. | A replay returns the original response, including its original `queued` state. Follow it with a GET when you need current progress. Authorization is checked again, so a stored receipt does not bypass revoked membership or protected-environment rules. After 24 hours, a key may be processed as a new operation. Reconcile existing state before resubmitting old work. ## Retry policy [#retry-policy] Retry reads and supported keyed writes after transport failures, 429, or transient 5xx responses. Use exponential backoff with jitter, a finite attempt limit, and a deadline. Honor `Retry-After`; do not retry earlier because the delay exceeds your own preferred backoff. Resolve validation, authentication, permission, and conflict errors before retrying. Keep the original key and payload after an uncertain write response. The Python and TypeScript SDKs generate a key for each supported write invocation and preserve it across automatic retries. A separate method invocation generates a new key unless you supply your own. Use a durable caller-supplied key for retries across process restarts. ## Reconciliation cases [#reconciliation-cases] `idempotency_legacy_conflict` identifies a deployment key without a retained response receipt. Read that deployment before starting another one. `idempotency_authorization_changed` means your workspace role changed after the original request. Reconcile the operation under your current authority before deciding to submit new work. Neither conflict is a reason to generate a fresh key automatically. # Read and follow logs (/api/logs) Read logs at `GET /workspaces/{workspaceId}/services/{serviceId}/logs`. Responses contain retained build, runtime, or system output available to the caller. ## Read the newest batch [#read-the-newest-batch] ```bash curl --fail-with-body --get \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ --data-urlencode 'source=runtime' \ --data-urlencode 'limit=100' \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/services/$OPENSTEAD_SERVICE_ID/logs" ``` ```json { "available": true, "items": [ { "id": 42, "timestamp": "2026-09-28T12:00:00Z", "source": "runtime", "message": "Application ready", "level": "info" } ], "cursor": 42, "hasMore": false, "limit": 100 } ``` The first request retrieves the newest retained batch, ordered by numeric ID ascending. It is not an export of all historical lines. ## Follow later lines [#follow-later-lines] Pass the previous response's numeric `cursor` as `after`. Preserve the other filters: ```text GET .../logs?source=runtime&limit=100&after=42 ``` When `hasMore` is true, request the next batch immediately. When false, pause before polling again. A cursor should advance when new matching lines are returned. The SDKs detect non-advancing continuation loops. ## Filters [#filters] | Query | Meaning | | ------------ | --------------------------------------------------------- | | `limit` | 1–500 lines; default 500. | | `after` | Numeric log ID to continue after. | | `source` | `build`, `runtime`, or `system`. | | `deployment` | A deployment UUID belonging to this service. | | `search` | Text filter, up to 200 characters. | | `hours` | Time window; default 24, bounded by the plan's retention. | Persist the service, cursor, and filters together when resuming a reader. Changes to filters, retention expiry, and a late restart can leave gaps. There is no collection-style `page` envelope or opaque cursor here. ## SDK helpers [#sdk-helpers] Python `logs.iter()` drains available batches and `logs.tail()` polls for new lines. TypeScript provides `logs.batches()` and `logs.tail()`. ```python import os from openstead import PollTimeout, Openstead with Openstead() as client: try: for line in client.logs.tail( workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], service_id=os.environ["OPENSTEAD_SERVICE_ID"], source="runtime", timeout=30.0, ): print(line.timestamp, line.level, line.message) except PollTimeout: pass ``` Choose a suitable destination for output: applications can log credentials, customer data, and other sensitive information. ## Unavailable logs [#unavailable-logs] A response may return `available: false`, an empty `items` array, and a reason without cursor metadata. Inspect that response before interpreting a quiet log reader as a healthy application. Tailing is HTTP polling over retained records, with no guarantee of an exhaustive archive. # API overview (/api/overview) The Openstead API lets your server, CI pipeline, or developer tools manage applications on Openstead. Openstead operates the infrastructure; API clients work with workspaces and services. ## Base URL [#base-url] ```text https://api.openstead.tech/api/v1 ``` Requests and responses use JSON. Send a workspace-scoped bearer key for authenticated operations. The [OpenAPI specification](https://api.openstead.tech/api/v1/openapi.json) is available without a key. ## Make your first request [#make-your-first-request] Create a read-only key in **Account settings → API Keys** and set `OPENSTEAD_API_KEY` in your shell's secret environment. Set `OPENSTEAD_WORKSPACE_ID` to the workspace UUID selected when creating that key. ```bash curl --fail-with-body \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/services?limit=100" ``` The response contains `services` and pagination metadata. A workspace with no services returns an empty list. Resource identifiers are UUIDs; a service name cannot replace a `serviceId` in a REST path. ## Resource groups [#resource-groups] | Resource | What you can do | | ----------- | ------------------------------------------------------------------------------ | | Catalog | Read service types, runtimes, plans, defaults, and current capabilities. | | Workspace | Read the authorized workspace, your role, and entitlements. | | Projects | Create and organise projects and read their environments. | | Services | Create or update configuration; archive, restore, delete, or operate services. | | Variables | Manage encrypted variables and explicitly reveal an authorized value. | | Deployments | Queue, inspect, cancel, or roll back releases. | | Logs | Read and follow retained build, runtime, and system output. | The public core specification defines 28 operations. Account sign-in, payment checkout, and dashboard-specific endpoints are separate workflows. Use the published specification when building a REST integration. ## Read the returned state [#read-the-returned-state] Success responses preserve a named envelope, such as `{"service": {...}}` or `{"projects": [...]}`. Create requests and queued actions return **HTTP 200**. Inspect the resource state to determine what completed. A deployment with `status: "queued"` has been accepted for processing. A deletion with `queued: true` is still in progress. Use follow-up reads or an SDK polling helper to observe the result. ## Choose your integration [#choose-your-integration] * [Python SDK](/integrations/python-sdk): synchronous and asynchronous typed clients. * [TypeScript SDK](/integrations/typescript-sdk): typed Node.js client with pagination and polling. * [Openstead CLI](/integrations/cli): interactive operations and CI commands. * [MCP server](/integrations/mcp): connect supported assistant tools to a workspace. * [API reference](/api/reference): endpoint parameters and response schemas. Before adding automation, read [authentication](/api/authentication), [pagination](/api/pagination), and [retry rules](/api/idempotency). # Pagination (/api/pagination) Projects, services, variables, and deployments support cursor pagination. Request an explicit limit and continue using the returned cursor until the collection ends. ## Request a page [#request-a-page] ```bash curl --fail-with-body \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/projects?limit=100" ``` ```json { "projects": [], "page": { "limit": 100, "nextCursor": null, "hasMore": false } } ``` The example represents an empty final page. When `hasMore` is `true`, `nextCursor` contains an opaque continuation token. ## Continue the same collection [#continue-the-same-collection] Set `CURSOR` to the exact `page.nextCursor` returned by the preceding request. ```bash curl --fail-with-body --get \ --header "Authorization: Bearer $OPENSTEAD_API_KEY" \ --data-urlencode "limit=100" \ --data-urlencode "cursor=$CURSOR" \ "https://api.openstead.tech/api/v1/workspaces/$OPENSTEAD_WORKSPACE_ID/projects" ``` Keep the caller, workspace, path, and filters the same. URL-encode the token; do not decode, edit, or manufacture it. An incompatible cursor returns `400 invalid_cursor`. ## Limits and ordering [#limits-and-ordering] | Collection | Without pagination parameters | With `limit` or `cursor` | | ----------------- | ------------------------------------------------ | ------------------------ | | Projects | Complete legacy list; no `page` field | Default 100; maximum 200 | | Services | Complete legacy list, including archived entries | Default 100; maximum 200 | | Service variables | Complete masked metadata list | Default 100; maximum 200 | | Deployments | Up to 100 records with page metadata | Maximum 200 | Limits must be integers from 1 to 200. Paginated results are ordered by creation time descending, then ID descending. Pagination is not a frozen snapshot: new records created after the first page may need a fresh listing. ## Use an SDK iterator [#use-an-sdk-iterator] ```python import os from openstead import Openstead with Openstead() as client: for service in client.services.iter( workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], limit=100, ): print(service.id, service.name) ``` ```ts import Openstead from '@layerrail/openstead'; const client = new Openstead(); for await (const service of client.services.iterate({ workspaceId: process.env.OPENSTEAD_WORKSPACE_ID!, })) { console.log(service.id, service.name); } ``` SDK iterators follow bounded pages and detect non-advancing cursors. A plain `list()` returns a single API response. ## Logs use a separate cursor [#logs-use-a-separate-cursor] [Log readers](/api/logs) use numeric `after` and `cursor` values, with a maximum limit of 500. Do not send a collection's opaque cursor to a log endpoint. # Get scoped service topology and shared layout (/api/reference/canvas/getWorkspaceCanvas) **GET /workspaces/{workspaceId}/canvas** Operation ID: `getWorkspaceCanvas`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/canvas` Readers can view the workspace, one project across its environments, or one project environment. Only active service records and valid scoped reference/group connections are shown; archived and deleting/deleted services are excluded. Manual secret values are never inspected. Layouts are independent per scope. Unsaved layouts have revision 0. No pagination or silent truncation: scopes beyond 2000 total nodes or 10000 reference records/edges return 413 canvas_too_large. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `projectId` | query | No | string | Optional project in this workspace; omit for all workspace services. Format: `uuid`. | | `environmentId` | query | No | string | Optional environment; requires projectId and must belong to that project. Format: `uuid`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [CanvasResponse](#schema-canvasresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: CanvasResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `edges` | Yes | array of [CanvasEdge](#schema-canvasedge) | Maximum items: `10000`. | | `groups` | Yes | array of [CanvasGroup](#schema-canvasgroup) | Maximum items: `2000`. | | `layout` | Yes | [CanvasLayout](#schema-canvaslayout) | | | `nodes` | Yes | array of [Service](#schema-service) | Maximum items: `2000`. | | `scope` | Yes | object | | | `scope.environmentId` | Yes | string or null | | | `scope.projectId` | Yes | string or null | | ### Schema: CanvasEdge Type: object. Configured metadata only, not evidence of live traffic. Reference edges point from consumer to provider and expose variable names only. Environment-group edges point from the group node to a linked service. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `id` | Yes | string | | | `keys` | No | array of string | | | `kind` | Yes | string | Allowed: `"reference"`, `"environment-group"`. | | `source` | Yes | string | | | `target` | Yes | string | | ### Schema: CanvasGroup Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `environmentId` | Yes | string or null | | | `id` | Yes | string | Pattern: `^group:`. | | `name` | Yes | string | | | `serviceIds` | Yes | array of string | | ### Schema: CanvasLayout Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `positions` | Yes | object | | | `revision` | Yes | integer | Minimum: `0`. Maximum: `9007199254740991`. | | `viewport` | Yes | object or null | | Additional properties are not accepted. ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Save the shared visual layout (/api/reference/canvas/saveWorkspaceCanvas) **PATCH /workspaces/{workspaceId}/canvas** Operation ID: `saveWorkspaceCanvas`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/canvas` Developer, admin or owner required, including in protected environments because this changes visual state only. Replaces all saved positions and viewport for the selected scope. A stale revision returns 409 canvas_revision_conflict; GET the latest canvas before retrying. Unknown or no-longer-visible node IDs return 400 canvas_invalid_node. Unknown scope parameters, duplicate/empty scope values and unsupported fields are rejected. Maximum request body is 256 KiB. Uses optimistic revision control, not Idempotency-Key replay. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `projectId` | query | No | string | Optional project in this workspace; omit for all workspace services. Format: `uuid`. | | `environmentId` | query | No | string | Optional environment; requires projectId and must belong to that project. Format: `uuid`. | ## Request body Required. Content type: `application/json`. Schema: [CanvasLayoutUpdate](#schema-canvaslayoutupdate). Example: ```json { "positions": {}, "revision": 0, "viewport": null } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [CanvasResponse](#schema-canvasresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: CanvasLayoutUpdate Type: object. Replace the visual layout for this scope using its current revision. Empty positions and null viewport reset the layout. Node IDs must be present in the current canvas. No connection, service or infrastructure configuration is changed. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `positions` | Yes | object | | | `revision` | Yes | integer | Minimum: `0`. Maximum: `9007199254740990`. | | `viewport` | Yes | object or null | | Additional properties are not accepted. ### Schema: CanvasResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `edges` | Yes | array of [CanvasEdge](#schema-canvasedge) | Maximum items: `10000`. | | `groups` | Yes | array of [CanvasGroup](#schema-canvasgroup) | Maximum items: `2000`. | | `layout` | Yes | [CanvasLayout](#schema-canvaslayout) | | | `nodes` | Yes | array of [Service](#schema-service) | Maximum items: `2000`. | | `scope` | Yes | object | | | `scope.environmentId` | Yes | string or null | | | `scope.projectId` | Yes | string or null | | ### Schema: CanvasEdge Type: object. Configured metadata only, not evidence of live traffic. Reference edges point from consumer to provider and expose variable names only. Environment-group edges point from the group node to a linked service. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `id` | Yes | string | | | `keys` | No | array of string | | | `kind` | Yes | string | Allowed: `"reference"`, `"environment-group"`. | | `source` | Yes | string | | | `target` | Yes | string | | ### Schema: CanvasGroup Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `environmentId` | Yes | string or null | | | `id` | Yes | string | Pattern: `^group:`. | | `name` | Yes | string | | | `serviceIds` | Yes | array of string | | ### Schema: CanvasLayout Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `positions` | Yes | object | | | `revision` | Yes | integer | Minimum: `0`. Maximum: `9007199254740991`. | | `viewport` | Yes | object or null | | Additional properties are not accepted. ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get supported configuration and runtime capabilities (/api/reference/catalog/getCatalog) **GET /catalog** Operation ID: `getCatalog`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/catalog` Requires authentication. Read capabilities from this environment; workerOnline reports recent worker readiness. Listed service types are not a guarantee of available capacity. Prices are estimates. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [Catalog](#schema-catalog). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: Catalog Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `capabilities` | Yes | object | | | `databasePlans` | Yes | array of object | | | `databasePlans[].backupRetentionDays` | No | integer | | | `databasePlans[].buildMinutes` | No | number | | | `databasePlans[].cpu` | No | number | | | `databasePlans[].id` | Yes | string | | | `databasePlans[].kinds` | No | array of string | | | `databasePlans[].logDays` | No | number | | | `databasePlans[].maxConnections` | No | integer | | | `databasePlans[].memoryMb` | No | number | | | `databasePlans[].name` | Yes | string | | | `databasePlans[].ngn` | Yes | number | | | `databasePlans[].storageGb` | No | number | | | `databasePlans[].transferGb` | No | number | | | `databasePlans[].usd` | Yes | number | | | `databasePlans[].workspaceLimit` | No | integer | | | `defaults` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `plans` | Yes | array of object | | | `plans[].backupRetentionDays` | No | integer | | | `plans[].buildMinutes` | No | number | | | `plans[].cpu` | No | number | | | `plans[].id` | Yes | string | | | `plans[].kinds` | No | array of string | | | `plans[].logDays` | No | number | | | `plans[].maxConnections` | No | integer | | | `plans[].memoryMb` | No | number | | | `plans[].name` | Yes | string | | | `plans[].ngn` | Yes | number | | | `plans[].storageGb` | No | number | | | `plans[].transferGb` | No | number | | | `plans[].usd` | Yes | number | | | `plans[].workspaceLimit` | No | integer | | | `pricesAreEstimates` | Yes | boolean | | | `regions` | Yes | array of object | | | `regions[].description` | Yes | string | | | `regions[].id` | Yes | string | | | `regions[].name` | Yes | string | | | `runtimes` | Yes | array of string | | | `serviceTypes` | Yes | array of object | | | `serviceTypes[].description` | Yes | string | | | `serviceTypes[].id` | Yes | string | | | `serviceTypes[].name` | Yes | string | | | `serviceTypes[].plural` | Yes | string | | | `serviceTypes[].runtime` | Yes | boolean | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get this public core API contract (/api/reference/catalog/getOpenApiSchema) **GET /openapi.json** Operation ID: `getOpenApiSchema`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/openapi.json` Request parameters, responses, and examples for get this public core api contract. ## Authentication No authentication is required for this operation. ## Parameters This operation has no path, query, or operation-specific header parameters. ## Request body No request body. ## Responses ### HTTP 200 OpenAPI 3.1 JSON document. No authentication required. Content type: `application/json`. Schema: object. Required keys: `openapi`, `info`, `paths`. ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Request deployment cancellation (/api/reference/deployments/cancelDeployment) **POST /workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}/cancel** Operation ID: `cancelDeployment`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}/cancel` Terminal deployments reject new cancellation requests. cancelRequested:true acknowledges the request; a running worker must still stop safely. Poll the deployment for its terminal state. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `deploymentId` | path | Yes | string | Deployment UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Optional. Content type: `application/json`. Schema: [OptionalRequest](#schema-optionalrequest). Example: ```json {} ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [CancelResponse](#schema-cancelresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: OptionalRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: CancelResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `cancelRequested` | Yes | boolean | Value: `true`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Queue a deployment (/api/reference/deployments/createDeployment) **POST /workspaces/{workspaceId}/services/{serviceId}/deploys** Operation ID: `createDeployment`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/deploys` Creates a durable deployment/job when hosting is enabled, subject to capacity, billing and source validation. The initial status is queued. Poll getDeployment for progress; use getService for the serving release. A replay returns the original successful response, not the latest status. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Optional. Content type: `application/json`. Schema: [DeploymentCreate](#schema-deploymentcreate). Example: ```json {} ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [DeploymentResponse](#schema-deploymentresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: DeploymentCreate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `commitSha` | No | string | Optional full commit SHA. Omit to resolve the configured branch. Pattern: `^([0-9a-f]{40,64})?$`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: DeploymentResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deployment` | Yes | [Deployment](#schema-deployment) | | Example: ```json { "deployment": { "actor": "developer@example.com", "blockedReason": "", "branch": "main", "canRollback": false, "cancelRequested": false, "commitMessage": "", "commitSha": "", "createdAt": "2026-09-25T12:00:00Z", "error": "", "finishedAt": null, "id": "00000000-0000-4000-8000-000000000001", "sourceDeploymentId": null, "startedAt": null, "status": "queued", "steps": [], "trigger": "manual" } } ``` ### Schema: Deployment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `actor` | Yes | string | | | `blockedReason` | Yes | string | | | `branch` | Yes | string | | | `canRollback` | Yes | boolean | | | `cancelRequested` | Yes | boolean | | | `commitMessage` | Yes | string | | | `commitSha` | Yes | string | | | `createdAt` | Yes | string | Format: `date-time`. | | `error` | Yes | string | | | `finishedAt` | Yes | string or null | | | `id` | Yes | string | Format: `uuid`. | | `sourceDeploymentId` | Yes | string or null | | | `startedAt` | Yes | string or null | | | `status` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps` | Yes | array of object | | | `steps[].finishedAt` | No | string | Format: `date-time`. | | `steps[].name` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps[].startedAt` | Yes | string | Format: `date-time`. | | `trigger` | Yes | string | | ### Schema: DeploymentStatus Type: string. Current release states. live, failed, cancelled and superseded are terminal; queued is not deployment success. Clients should tolerate future response states. Allowed: `"queued"`, `"preparing"`, `"cloning"`, `"building"`, `"predeploy"`, `"deploying"`, `"checking"`, `"live"`, `"failed"`, `"cancelled"`, `"superseded"`. ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get deployment progress (/api/reference/deployments/getDeployment) **GET /workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}** Operation ID: `getDeployment`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}` Request parameters, responses, and examples for get deployment progress. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `deploymentId` | path | Yes | string | Deployment UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [DeploymentResponse](#schema-deploymentresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: DeploymentResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deployment` | Yes | [Deployment](#schema-deployment) | | Example: ```json { "deployment": { "actor": "developer@example.com", "blockedReason": "", "branch": "main", "canRollback": false, "cancelRequested": false, "commitMessage": "", "commitSha": "", "createdAt": "2026-09-25T12:00:00Z", "error": "", "finishedAt": null, "id": "00000000-0000-4000-8000-000000000001", "sourceDeploymentId": null, "startedAt": null, "status": "queued", "steps": [], "trigger": "manual" } } ``` ### Schema: Deployment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `actor` | Yes | string | | | `blockedReason` | Yes | string | | | `branch` | Yes | string | | | `canRollback` | Yes | boolean | | | `cancelRequested` | Yes | boolean | | | `commitMessage` | Yes | string | | | `commitSha` | Yes | string | | | `createdAt` | Yes | string | Format: `date-time`. | | `error` | Yes | string | | | `finishedAt` | Yes | string or null | | | `id` | Yes | string | Format: `uuid`. | | `sourceDeploymentId` | Yes | string or null | | | `startedAt` | Yes | string or null | | | `status` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps` | Yes | array of object | | | `steps[].finishedAt` | No | string | Format: `date-time`. | | `steps[].name` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps[].startedAt` | Yes | string | Format: `date-time`. | | `trigger` | Yes | string | | ### Schema: DeploymentStatus Type: string. Current release states. live, failed, cancelled and superseded are terminal; queued is not deployment success. Clients should tolerate future response states. Allowed: `"queued"`, `"preparing"`, `"cloning"`, `"building"`, `"predeploy"`, `"deploying"`, `"checking"`, `"live"`, `"failed"`, `"cancelled"`, `"superseded"`. ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # List deployments, newest first (/api/reference/deployments/listDeployments) **GET /workspaces/{workspaceId}/services/{serviceId}/deploys** Operation ID: `listDeployments`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/deploys` Bounded by default to 100; maximum limit 200. Follow page.nextCursor for older deployments. Hosting disabled with no runtime returns available:false and empty items. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `limit` | query | No | integer | Page size. Supplying limit or cursor enables pagination for legacy unbounded lists. Minimum: `1`. Maximum: `200`. Default: `100`. | | `cursor` | query | No | string | Opaque nextCursor returned by the previous page. Reuse with the same endpoint and query; do not parse or construct it. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [DeploymentList](#schema-deploymentlist). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: DeploymentList Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `available` | Yes | boolean | | | `items` | Yes | array of [Deployment](#schema-deployment) | | | `page` | Yes | [Page](#schema-page) | | | `reason` | No | string | | ### Schema: Deployment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `actor` | Yes | string | | | `blockedReason` | Yes | string | | | `branch` | Yes | string | | | `canRollback` | Yes | boolean | | | `cancelRequested` | Yes | boolean | | | `commitMessage` | Yes | string | | | `commitSha` | Yes | string | | | `createdAt` | Yes | string | Format: `date-time`. | | `error` | Yes | string | | | `finishedAt` | Yes | string or null | | | `id` | Yes | string | Format: `uuid`. | | `sourceDeploymentId` | Yes | string or null | | | `startedAt` | Yes | string or null | | | `status` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps` | Yes | array of object | | | `steps[].finishedAt` | No | string | Format: `date-time`. | | `steps[].name` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps[].startedAt` | Yes | string | Format: `date-time`. | | `trigger` | Yes | string | | ### Schema: DeploymentStatus Type: string. Current release states. live, failed, cancelled and superseded are terminal; queued is not deployment success. Clients should tolerate future response states. Allowed: `"queued"`, `"preparing"`, `"cloning"`, `"building"`, `"predeploy"`, `"deploying"`, `"checking"`, `"live"`, `"failed"`, `"cancelled"`, `"superseded"`. ### Schema: Page Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `hasMore` | Yes | boolean | | | `limit` | Yes | integer | Minimum: `1`. Maximum: `200`. | | `nextCursor` | Yes | string or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Queue a rollback to a successful release (/api/reference/deployments/rollbackDeployment) **POST /workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}/rollback** Operation ID: `rollbackDeployment`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/deploys/{deploymentId}/rollback` The source must be a live deployment of this service with a retained image. Reuses its configuration and encrypted secret snapshot to queue a new deployment. confirm is the service name. This is not an in-place status edit. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `deploymentId` | path | Yes | string | Deployment UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ConfirmRequest](#schema-confirmrequest). Example: ```json { "confirm": "my-api" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [DeploymentResponse](#schema-deploymentresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ConfirmRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `confirm` | Yes | string | Minimum length: `1`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: DeploymentResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deployment` | Yes | [Deployment](#schema-deployment) | | Example: ```json { "deployment": { "actor": "developer@example.com", "blockedReason": "", "branch": "main", "canRollback": false, "cancelRequested": false, "commitMessage": "", "commitSha": "", "createdAt": "2026-09-25T12:00:00Z", "error": "", "finishedAt": null, "id": "00000000-0000-4000-8000-000000000001", "sourceDeploymentId": null, "startedAt": null, "status": "queued", "steps": [], "trigger": "manual" } } ``` ### Schema: Deployment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `actor` | Yes | string | | | `blockedReason` | Yes | string | | | `branch` | Yes | string | | | `canRollback` | Yes | boolean | | | `cancelRequested` | Yes | boolean | | | `commitMessage` | Yes | string | | | `commitSha` | Yes | string | | | `createdAt` | Yes | string | Format: `date-time`. | | `error` | Yes | string | | | `finishedAt` | Yes | string or null | | | `id` | Yes | string | Format: `uuid`. | | `sourceDeploymentId` | Yes | string or null | | | `startedAt` | Yes | string or null | | | `status` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps` | Yes | array of object | | | `steps[].finishedAt` | No | string | Format: `date-time`. | | `steps[].name` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps[].startedAt` | Yes | string | Format: `date-time`. | | `trigger` | Yes | string | | ### Schema: DeploymentStatus Type: string. Current release states. live, failed, cancelled and superseded are terminal; queued is not deployment success. Clients should tolerate future response states. Allowed: `"queued"`, `"preparing"`, `"cloning"`, `"building"`, `"predeploy"`, `"deploying"`, `"checking"`, `"live"`, `"failed"`, `"cancelled"`, `"superseded"`. ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # API reference (/api/reference) Use your workspace API key to manage projects, services, deployments, and configuration. [Start with authentication](/api/authentication) or [download the OpenAPI specification](/openapi.json). The interactive playground sends requests to your real Openstead workspace. Review the endpoint, identifiers, and request body before sending a request. ## Catalog [#catalog] * [Get supported configuration and runtime capabilities](/api/reference/catalog/getCatalog) * [Get this public core API contract](/api/reference/catalog/getOpenApiSchema) ## Workspaces [#workspaces] * [Get a workspace](/api/reference/workspaces/getWorkspace) ## Canvas [#canvas] * [Get scoped service topology and shared layout](/api/reference/canvas/getWorkspaceCanvas) * [Save the shared visual layout](/api/reference/canvas/saveWorkspaceCanvas) ## Projects [#projects] * [List projects](/api/reference/projects/listProjects) * [Create a project and its Production environment](/api/reference/projects/createProject) * [Delete an empty project](/api/reference/projects/deleteProject) * [Get a project](/api/reference/projects/getProject) * [Update a project](/api/reference/projects/updateProject) ## Services [#services] * [List services](/api/reference/services/listServices) * [Create a service](/api/reference/services/createService) * [Delete a service or queue infrastructure deletion](/api/reference/services/deleteService) * [Get service configuration and runtime state](/api/reference/services/getService) * [Update service configuration](/api/reference/services/updateService) * [Queue a service action](/api/reference/services/runServiceAction) * [Archive service configuration](/api/reference/services/archiveService) * [Get service operation progress](/api/reference/services/getServiceOperation) * [Restore archived service configuration](/api/reference/services/restoreService) ## Deployments [#deployments] * [List deployments, newest first](/api/reference/deployments/listDeployments) * [Queue a deployment](/api/reference/deployments/createDeployment) * [Get deployment progress](/api/reference/deployments/getDeployment) * [Request deployment cancellation](/api/reference/deployments/cancelDeployment) * [Queue a rollback to a successful release](/api/reference/deployments/rollbackDeployment) ## Logs [#logs] * [Read or tail service logs](/api/reference/logs/listServiceLogs) ## Variables [#variables] * [List masked variable metadata](/api/reference/variables/listServiceVariables) * [Create an encrypted variable](/api/reference/variables/createServiceVariable) * [Delete a variable](/api/reference/variables/deleteServiceVariable) * [Update a variable name or encrypted value](/api/reference/variables/updateServiceVariable) * [Reveal a variable with an audit event](/api/reference/variables/revealServiceVariable) # Read or tail service logs (/api/reference/logs/listServiceLogs) **GET /workspaces/{workspaceId}/services/{serviceId}/logs** Operation ID: `listServiceLogs`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/logs` Without after returns the newest retained batch in ascending numeric ID order. With after returns later lines in ascending ID order. Continue with response cursor; this is not opaque collection pagination. Retention depends on the plan, and search is limited to 200 characters. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `after` | query | No | integer | Numeric log cursor from the previous response. Minimum: `0`. Default: `0`. | | `limit` | query | No | integer | Maximum lines per batch. Minimum: `1`. Maximum: `500`. Default: `500`. | | `deployment` | query | No | string | Filter by deployment in this service. Format: `uuid`. | | `source` | query | No | string | Log source. Allowed: `"build"`, `"runtime"`, `"system"`. | | `search` | query | No | string | Case-insensitive message substring. Maximum length: `200`. | | `hours` | query | No | integer | Lookback window, clamped to at least one hour and the plan retention limit. Default: `24`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [LogList](#schema-loglist). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: LogList Type: object. Available streams include cursor, hasMore and limit. An unprovisioned service with hosting disabled returns available:false, empty items and reason. cursor is the numeric tail position for the next after query, not an opaque collection cursor. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `available` | Yes | boolean | | | `cursor` | No | integer | Minimum: `0`. | | `hasMore` | No | boolean | | | `items` | Yes | array of [LogLine](#schema-logline) | | | `limit` | No | integer | Minimum: `1`. Maximum: `500`. | | `reason` | No | string | | Example: ```json { "available": true, "cursor": 42, "hasMore": false, "items": [ { "id": 42, "level": "info", "message": "Application ready", "source": "runtime", "timestamp": "2026-09-25T12:00:00Z" } ], "limit": 500 } ``` ### Schema: LogLine Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `id` | Yes | integer | Minimum: `1`. | | `level` | Yes | string | | | `message` | Yes | string | | | `source` | Yes | string | Allowed: `"build"`, `"runtime"`, `"system"`. | | `timestamp` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Create a project and its Production environment (/api/reference/projects/createProject) **POST /workspaces/{workspaceId}/projects** Operation ID: `createProject`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/projects` Request parameters, responses, and examples for create a project and its production environment. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ProjectCreate](#schema-projectcreate). Example: ```json { "color": "violet", "name": "API platform" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ProjectResponse](#schema-projectresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ProjectCreate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | No | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `description` | No | string | Maximum length: `500`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `80`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ProjectResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `project` | Yes | [Project](#schema-project) | | Example: ```json { "project": { "color": "violet", "createdAt": "2026-09-25T12:00:00Z", "description": "", "environments": [], "id": "00000000-0000-4000-8000-000000000001", "name": "API platform", "updatedAt": "2026-09-25T12:00:00Z" } } ``` ### Schema: Project Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | Yes | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `createdAt` | Yes | string | Format: `date-time`. | | `description` | Yes | string | Maximum length: `500`. | | `environments` | Yes | array of [Environment](#schema-environment) | | | `id` | Yes | string | Format: `uuid`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `80`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: Environment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `isolated` | Yes | boolean | | | `name` | Yes | string | | | `projectId` | Yes | string | Format: `uuid`. | | `protected` | Yes | boolean | | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Delete an empty project (/api/reference/projects/deleteProject) **DELETE /workspaces/{workspaceId}/projects/{projectId}** Operation ID: `deleteProject`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/projects/{projectId}` Admin or owner required. Move/delete services and scoped environment groups first. confirm must exactly match the project name. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `projectId` | path | Yes | string | Project UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ConfirmRequest](#schema-confirmrequest). Example: ```json { "confirm": "API platform" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [Deleted](#schema-deleted). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ConfirmRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `confirm` | Yes | string | Minimum length: `1`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: Deleted Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deleted` | Yes | boolean | | | `operation` | No | [Operation](#schema-operation) | | | `queued` | No | boolean | | ### Schema: Operation Type: object. Safe job metadata only. Internal payload, results and error details are not included. Tolerate future operation status values. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `kind` | Yes | string | | | `status` | Yes | string | Allowed: `"queued"`, `"running"`, `"complete"`, `"failed"`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get a project (/api/reference/projects/getProject) **GET /workspaces/{workspaceId}/projects/{projectId}** Operation ID: `getProject`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/projects/{projectId}` Request parameters, responses, and examples for get a project. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `projectId` | path | Yes | string | Project UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ProjectResponse](#schema-projectresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ProjectResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `project` | Yes | [Project](#schema-project) | | Example: ```json { "project": { "color": "violet", "createdAt": "2026-09-25T12:00:00Z", "description": "", "environments": [], "id": "00000000-0000-4000-8000-000000000001", "name": "API platform", "updatedAt": "2026-09-25T12:00:00Z" } } ``` ### Schema: Project Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | Yes | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `createdAt` | Yes | string | Format: `date-time`. | | `description` | Yes | string | Maximum length: `500`. | | `environments` | Yes | array of [Environment](#schema-environment) | | | `id` | Yes | string | Format: `uuid`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `80`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: Environment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `isolated` | Yes | boolean | | | `name` | Yes | string | | | `projectId` | Yes | string | Format: `uuid`. | | `protected` | Yes | boolean | | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # List projects (/api/reference/projects/listProjects) **GET /workspaces/{workspaceId}/projects** Operation ID: `listProjects`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/projects` Legacy requests without limit/cursor return the complete list. Opt in to bounded pages with limit or cursor. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `limit` | query | No | integer | Page size. Supplying limit or cursor enables pagination for legacy unbounded lists. Minimum: `1`. Maximum: `200`. Default: `100`. | | `cursor` | query | No | string | Opaque nextCursor returned by the previous page. Reuse with the same endpoint and query; do not parse or construct it. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ProjectList](#schema-projectlist). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ProjectList Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `page` | No | [Page](#schema-page) | | | `projects` | Yes | array of [Project](#schema-project) | | Example: ```json { "page": { "hasMore": false, "limit": 100, "nextCursor": null }, "projects": [ { "color": "violet", "createdAt": "2026-09-25T12:00:00Z", "description": "", "environments": [], "id": "00000000-0000-4000-8000-000000000001", "name": "API platform", "updatedAt": "2026-09-25T12:00:00Z" } ] } ``` ### Schema: Page Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `hasMore` | Yes | boolean | | | `limit` | Yes | integer | Minimum: `1`. Maximum: `200`. | | `nextCursor` | Yes | string or null | | ### Schema: Project Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | Yes | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `createdAt` | Yes | string | Format: `date-time`. | | `description` | Yes | string | Maximum length: `500`. | | `environments` | Yes | array of [Environment](#schema-environment) | | | `id` | Yes | string | Format: `uuid`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `80`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: Environment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `isolated` | Yes | boolean | | | `name` | Yes | string | | | `projectId` | Yes | string | Format: `uuid`. | | `protected` | Yes | boolean | | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Update a project (/api/reference/projects/updateProject) **PATCH /workspaces/{workspaceId}/projects/{projectId}** Operation ID: `updateProject`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/projects/{projectId}` Request parameters, responses, and examples for update a project. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `projectId` | path | Yes | string | Project UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ProjectUpdate](#schema-projectupdate). Example: ```json { "description": "Public API and workers" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ProjectResponse](#schema-projectresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ProjectUpdate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | No | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `description` | No | string | Maximum length: `500`. | | `name` | No | string | Minimum length: `1`. Maximum length: `80`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ProjectResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `project` | Yes | [Project](#schema-project) | | Example: ```json { "project": { "color": "violet", "createdAt": "2026-09-25T12:00:00Z", "description": "", "environments": [], "id": "00000000-0000-4000-8000-000000000001", "name": "API platform", "updatedAt": "2026-09-25T12:00:00Z" } } ``` ### Schema: Project Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `color` | Yes | string | Allowed: `"violet"`, `"blue"`, `"green"`, `"orange"`, `"pink"`, `"gray"`. | | `createdAt` | Yes | string | Format: `date-time`. | | `description` | Yes | string | Maximum length: `500`. | | `environments` | Yes | array of [Environment](#schema-environment) | | | `id` | Yes | string | Format: `uuid`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `80`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: Environment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `isolated` | Yes | boolean | | | `name` | Yes | string | | | `projectId` | Yes | string | Format: `uuid`. | | `protected` | Yes | boolean | | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Archive service configuration (/api/reference/services/archiveService) **POST /workspaces/{workspaceId}/services/{serviceId}/archive** Operation ID: `archiveService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/archive` Sets archived:true and prevents deployment. This is not a runtime suspend or infrastructure deletion operation. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Optional. Content type: `application/json`. Schema: [OptionalRequest](#schema-optionalrequest). Example: ```json {} ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceResponse](#schema-serviceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: OptionalRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ServiceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `service` | Yes | [Service](#schema-service) | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Create a service (/api/reference/services/createService) **POST /workspaces/{workspaceId}/services** Operation ID: `createService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services` Creation saves configuration by default. deploy:true also queues real execution if enabled and authorized. Project placement requires an environment belonging to that project. Protected environments require admin access. API keys cannot grant paid-compute or automatic-payment consent. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ServiceCreate](#schema-servicecreate). Example: ```json { "configuration": { "buildMethod": "railpack", "plan": "free", "port": 8000, "repository": "https://github.com/your-team/your-app", "runtime": "auto", "sourceType": "repository" }, "deploy": false, "kind": "web", "name": "my-api", "variables": [ { "key": "APP_ENV", "value": "production" } ] } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceResponse](#schema-serviceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ServiceCreate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `automaticPaymentConsent` | No | string | Legacy field. Automatic monetary collection is disabled. Purchase service months through the dashboard. Deprecated. | | `billingConsent` | No | string | Legacy field. Paid compute now requires a settled prepaid service term purchased in the dashboard; this field cannot grant access. Deprecated. | | `configuration` | No | [ServiceConfigurationInput](#schema-serviceconfigurationinput) | | | `deploy` | No | boolean | If true, queue a real deployment in the same creation transaction. Failure rolls creation back. Default: `false`. | | `disk` | No | object | | | `disk.configuration` | No | object | | | `disk.configuration.mountPath` | No | string | | | `disk.configuration.sizeGb` | No | integer | Minimum: `5`. Maximum: `1000`. | | `disk.name` | Yes | string | | | `environmentId` | No | string or null | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | No | string or null | | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | | `secretFiles` | No | array of object | Maximum items: `20`. | | `secretFiles[].key` | Yes | string | Maximum length: `255`. | | `secretFiles[].value` | Yes | string | UTF-8 text, at most 65536 bytes. Never returned by CRUD responses. Write only. | | `variables` | No | array of [VariableCreate](#schema-variablecreate) | Maximum items: `100`. | ### Schema: ServiceConfigurationInput Type: object. Partial configuration on create/update; omitted fields retain defaults/current values. Constraints also depend on service kind, plan and workspace entitlements. Unknown configuration keys are rejected. Credentials belong in encrypted variables/secret files. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are not accepted. ### Schema: VariableCreate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `key` | Yes | string | Maximum length: `255`. Pattern: `^[A-Za-z_][A-Za-z0-9_]*$`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | | `value` | Yes | string | UTF-8 text, at most 65536 bytes. Never returned by CRUD responses. Write only. | ### Schema: ServiceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `service` | Yes | [Service](#schema-service) | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Delete a service or queue infrastructure deletion (/api/reference/services/deleteService) **DELETE /workspaces/{workspaceId}/services/{serviceId}** Operation ID: `deleteService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}` Without runtime infrastructure returns deleted:true. With runtime infrastructure archives the service and queues deletion, returning deleted:false, queued:true. HTTP 200 is preserved; queued does not mean deletion finished. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ConfirmRequest](#schema-confirmrequest). Example: ```json { "confirm": "my-api" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [Deleted](#schema-deleted). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ConfirmRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `confirm` | Yes | string | Minimum length: `1`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: Deleted Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deleted` | Yes | boolean | | | `operation` | No | [Operation](#schema-operation) | | | `queued` | No | boolean | | ### Schema: Operation Type: object. Safe job metadata only. Internal payload, results and error details are not included. Tolerate future operation status values. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `kind` | Yes | string | | | `status` | Yes | string | Allowed: `"queued"`, `"running"`, `"complete"`, `"failed"`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get service configuration and runtime state (/api/reference/services/getService) **GET /workspaces/{workspaceId}/services/{serviceId}** Operation ID: `getService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}` Request parameters, responses, and examples for get service configuration and runtime state. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceResponse](#schema-serviceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ServiceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `service` | Yes | [Service](#schema-service) | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get service operation progress (/api/reference/services/getServiceOperation) **GET /workspaces/{workspaceId}/services/{serviceId}/operations/{operationId}** Operation ID: `getServiceOperation`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/operations/{operationId}` Returns the actual queued/running/complete/failed state of a job belonging to this workspace and service. Internal payloads and error details are not exposed. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `operationId` | path | Yes | string | Operation UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [OperationResponse](#schema-operationresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: OperationResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `operation` | Yes | [Operation](#schema-operation) | | ### Schema: Operation Type: object. Safe job metadata only. Internal payload, results and error details are not included. Tolerate future operation status values. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `kind` | Yes | string | | | `status` | Yes | string | Allowed: `"queued"`, `"running"`, `"complete"`, `"failed"`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # List services (/api/reference/services/listServices) **GET /workspaces/{workspaceId}/services** Operation ID: `listServices`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services` Includes archived services. Legacy requests without limit/cursor return the complete list. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `limit` | query | No | integer | Page size. Supplying limit or cursor enables pagination for legacy unbounded lists. Minimum: `1`. Maximum: `200`. Default: `100`. | | `cursor` | query | No | string | Opaque nextCursor returned by the previous page. Reuse with the same endpoint and query; do not parse or construct it. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceList](#schema-servicelist). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ServiceList Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `page` | No | [Page](#schema-page) | | | `services` | Yes | array of [Service](#schema-service) | | ### Schema: Page Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `hasMore` | Yes | boolean | | | `limit` | Yes | integer | Minimum: `1`. Maximum: `200`. | | `nextCursor` | Yes | string or null | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Restore archived service configuration (/api/reference/services/restoreService) **POST /workspaces/{workspaceId}/services/{serviceId}/restore** Operation ID: `restoreService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/restore` Sets archived:false; does not automatically deploy or resume infrastructure. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Optional. Content type: `application/json`. Schema: [OptionalRequest](#schema-optionalrequest). Example: ```json {} ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceResponse](#schema-serviceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: OptionalRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ServiceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `service` | Yes | [Service](#schema-service) | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Queue a service action (/api/reference/services/runServiceAction) **POST /workspaces/{workspaceId}/services/{serviceId}/actions** Operation ID: `runServiceAction`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/actions` restart, suspend and resume return queued:true and a pollable operation. deploy and clear-cache return a deployment. Hosting must be enabled; runtime actions require an existing provisioned runtime. HTTP 200 does not mean execution has completed. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ServiceAction](#schema-serviceaction). Example: ```json { "action": "restart" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ActionResponse](#schema-actionresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ServiceAction Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `action` | Yes | string | Allowed: `"restart"`, `"suspend"`, `"resume"`, `"deploy"`, `"clear-cache"`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ActionResponse Type: [QueuedOperation](#schema-queuedoperation) or [DeploymentResponse](#schema-deploymentresponse). restart/suspend/resume return queued and operation. deploy/clear-cache return deployment. ### Schema: QueuedOperation Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `operation` | Yes | [Operation](#schema-operation) | | | `queued` | Yes | boolean | Value: `true`. | Example: ```json { "operation": { "createdAt": "2026-09-25T12:00:00Z", "id": "00000000-0000-4000-8000-000000000001", "kind": "restart", "status": "queued", "updatedAt": "2026-09-25T12:00:00Z" }, "queued": true } ``` ### Schema: Operation Type: object. Safe job metadata only. Internal payload, results and error details are not included. Tolerate future operation status values. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `kind` | Yes | string | | | `status` | Yes | string | Allowed: `"queued"`, `"running"`, `"complete"`, `"failed"`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: DeploymentResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deployment` | Yes | [Deployment](#schema-deployment) | | Example: ```json { "deployment": { "actor": "developer@example.com", "blockedReason": "", "branch": "main", "canRollback": false, "cancelRequested": false, "commitMessage": "", "commitSha": "", "createdAt": "2026-09-25T12:00:00Z", "error": "", "finishedAt": null, "id": "00000000-0000-4000-8000-000000000001", "sourceDeploymentId": null, "startedAt": null, "status": "queued", "steps": [], "trigger": "manual" } } ``` ### Schema: Deployment Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `actor` | Yes | string | | | `blockedReason` | Yes | string | | | `branch` | Yes | string | | | `canRollback` | Yes | boolean | | | `cancelRequested` | Yes | boolean | | | `commitMessage` | Yes | string | | | `commitSha` | Yes | string | | | `createdAt` | Yes | string | Format: `date-time`. | | `error` | Yes | string | | | `finishedAt` | Yes | string or null | | | `id` | Yes | string | Format: `uuid`. | | `sourceDeploymentId` | Yes | string or null | | | `startedAt` | Yes | string or null | | | `status` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps` | Yes | array of object | | | `steps[].finishedAt` | No | string | Format: `date-time`. | | `steps[].name` | Yes | [DeploymentStatus](#schema-deploymentstatus) | | | `steps[].startedAt` | Yes | string | Format: `date-time`. | | `trigger` | Yes | string | | ### Schema: DeploymentStatus Type: string. Current release states. live, failed, cancelled and superseded are terminal; queued is not deployment success. Clients should tolerate future response states. Allowed: `"queued"`, `"preparing"`, `"cloning"`, `"building"`, `"predeploy"`, `"deploying"`, `"checking"`, `"live"`, `"failed"`, `"cancelled"`, `"superseded"`. ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Update service configuration (/api/reference/services/updateService) **PATCH /workspaces/{workspaceId}/services/{serviceId}** Operation ID: `updateService`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}` Merges configuration fields; does not start a new deployment. Some routing settings enqueue routing reconciliation. A service kind cannot change. Environment moves must preserve valid variable-group scope. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [ServiceUpdate](#schema-serviceupdate). Example: ```json { "configuration": { "healthCheckPath": "/health" } } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ServiceResponse](#schema-serviceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: ServiceUpdate Type: object. PATCH merges supplied configuration fields. kind is immutable; send the current value or omit it. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `configuration` | No | [ServiceConfigurationInput](#schema-serviceconfigurationinput) | | | `environmentId` | No | string or null | | | `kind` | No | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `name` | No | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | No | string or null | | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: ServiceConfigurationInput Type: object. Partial configuration on create/update; omitted fields retain defaults/current values. Constraints also depend on service kind, plan and workspace entitlements. Unknown configuration keys are rejected. Credentials belong in encrypted variables/secret files. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are not accepted. ### Schema: ServiceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `service` | Yes | [Service](#schema-service) | | ### Schema: Service Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `archived` | Yes | boolean | | | `configuration` | Yes | [ServiceConfiguration](#schema-serviceconfiguration) | | | `createdAt` | Yes | string | Format: `date-time`. | | `deletedAt` | Yes | string or null | | | `deletionRequestedAt` | Yes | string or null | | | `deploymentId` | Yes | string or null | | | `deploymentUpdatedAt` | Yes | string or null | | | `displayStatus` | Yes | string | | | `environmentGroupIds` | Yes | array of string | | | `environmentId` | Yes | string or null | | | `estimatedMonthly` | Yes | [Estimate](#schema-estimate) | | | `healthCheckedAt` | Yes | string or null | | | `healthStatus` | Yes | string | Last readiness observation, or unknown after five minutes without a fresh observation. Allowed: `"ready"`, `"unavailable"`, `"degraded"`, `"unknown"`. | | `id` | Yes | string | Format: `uuid`. | | `internalHost` | Yes | string | | | `kind` | Yes | string | Allowed: `"static"`, `"web"`, `"private"`, `"worker"`, `"cron"`, `"postgres"`, `"mysql"`, `"redis"`, `"workflow"`. | | `lifecycleStatus` | Yes | string | | | `liveDeploymentId` | Yes | string or null | | | `name` | Yes | string | Minimum length: `1`. Maximum length: `63`. Pattern: `^[a-z0-9][a-z0-9-]*$`. | | `projectId` | Yes | string or null | | | `runtimeDetail` | Yes | string | | | `status` | Yes | string | | | `updatedAt` | Yes | string | Format: `date-time`. | | `url` | Yes | string or null | | ### Schema: ServiceConfiguration Type: object. Resolved service configuration. Clients must tolerate additive response fields. Creation and update validate ServiceConfigurationInput. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `autoDeploy` | No | string | Allowed: `"off"`, `"commit"`, `"checks"`. Maximum length: `2000`. Default: `"off"`. | | `backupEnabled` | No | boolean | Default: `true`. | | `backupRetentionDays` | No | integer | MySQL Free supports 1–7 days; other database plans support 1–30 days. Minimum: `1`. Maximum: `30`. Default: `7`. | | `branch` | No | string | Maximum length: `2000`. Default: `"main"`. | | `buildCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `buildMethod` | No | string | Allowed: `"railpack"`, `"dockerfile"`, `"image"`. Maximum length: `2000`. Default: `"railpack"`. | | `cacheProfile` | No | string | Allowed: `"none"`, `"safe"`, `"static"`. Maximum length: `2000`. Default: `"none"`. | | `databaseName` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseUser` | No | string | Maximum length: `2000`. Default: `""`. | | `databaseVersion` | No | string | PostgreSQL supports 16, 17 and 18. MySQL supports 8.4 LTS and defaults to 8.4. Existing managed databases cannot change major versions in place. Allowed: `"16"`, `"17"`, `"18"`, `"8.4"`. Maximum length: `2000`. Default: `"17"`. | | `description` | No | string | Maximum length: `2000`. Default: `""`. | | `dockerContext` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"."`. | | `dockerfilePath` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"Dockerfile"`. | | `evictionPolicy` | No | string | Allowed: `"allkeys-lru"`, `"allkeys-lfu"`, `"volatile-lru"`, `"noeviction"`. Maximum length: `2000`. Default: `"allkeys-lru"`. | | `healthCheckPath` | No | string | Maximum length: `2000`. Default: `""`. | | `ignorePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `image` | No | string | Maximum length: `2000`. Default: `""`. | | `includePaths` | No | array of string | Maximum items: `50`. Default: `[]`. | | `ipAllowList` | No | array of string | Maximum items: `50`. Default: `[]`. | | `maintenance` | No | boolean | Default: `false`. | | `maintenancePage` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `maxConcurrency` | No | integer | Minimum: `1`. Maximum: `100`. Default: `1`. | | `maxReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `3`. | | `maxRetries` | No | integer | Minimum: `0`. Maximum: `10`. Default: `3`. | | `minReplicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `notifications` | No | string | Allowed: `"workspace"`, `"all"`, `"failure"`, `"none"`. Maximum length: `2000`. Default: `"workspace"`. | | `plan` | No | string | MySQL defaults to mysql-free: one database per workspace, 768 MiB RAM, 0.25 shared CPU, 2 GiB persistent storage, 25 connections and up to 7 days of backups, with no card or expiry. PostgreSQL uses postgres-* plans; MySQL uses mysql-* plans; other kinds use compute plans. The free compute plan supports web/static only. Paid services require a confirmed service-month purchase through hosted checkout. Allowed: `"free"`, `"starter"`, `"builder"`, `"growth"`, `"scale"`, `"postgres-starter"`, `"postgres-standard"`, `"mysql-free"`, `"mysql-starter"`, `"mysql-standard"`. Maximum length: `2000`. Default: `"builder"`. | | `platformDomainEnabled` | No | boolean | Default: `true`. | | `port` | No | integer | Minimum: `1`. Maximum: `65535`. Default: `8000`. | | `preDeployCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `previewExpiryHours` | No | integer | Minimum: `1`. Maximum: `720`. Default: `72`. | | `previewMode` | No | string | Allowed: `"off"`, `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"off"`. | | `privateNetworking` | No | boolean | Default: `true`. | | `publishDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `"dist"`. | | `region` | No | string | Allowed: `"auto"`, `"eu"`. Maximum length: `2000`. Default: `"auto"`. | | `registryId` | No | string | Empty or the UUID of a registry integration in this workspace. Maximum length: `2000`. Default: `""`. | | `replicas` | No | integer | Minimum: `1`. Maximum: `20`. Default: `1`. | | `repository` | No | string | Empty or an HTTPS URL without embedded credentials. Maximum length: `1000`. Default: `""`. | | `rootDirectory` | No | string | Relative repository path without backslashes or parent traversal. Maximum length: `255`. Default: `""`. | | `runtime` | No | string | auto detects the application. Railpack is the default build method. Allowed: `"auto"`, `"docker"`, `"node"`, `"python"`, `"go"`, `"php"`, `"java"`, `"ruby"`, `"rust"`, `"elixir"`, `"deno"`, `"dotnet"`, `"gleam"`, `"cpp"`, `"static"`, `"shell"`. Maximum length: `2000`. Default: `"auto"`. | | `scaling` | No | string | Allowed: `"manual"`, `"automatic"`. Maximum length: `2000`. Default: `"manual"`. | | `schedule` | No | string | Cron expression validated for cron services; timezone uses an IANA identifier. Maximum length: `2000`. Default: `"0 * * * *"`. | | `sourceType` | No | string | Allowed: `"repository"`, `"image"`, `"none"`. Maximum length: `2000`. Default: `"repository"`. | | `startCommand` | No | string | Maximum length: `2000`. Default: `""`. | | `targetCpu` | No | integer | Minimum: `1`. Maximum: `100`. Default: `70`. | | `targetMemory` | No | integer | Minimum: `1`. Maximum: `100`. Default: `80`. | | `timeoutSeconds` | No | integer | Cron services have a lower maximum of 43200 seconds. Minimum: `1`. Maximum: `86400`. Default: `3600`. | | `timezone` | No | string | Maximum length: `2000`. Default: `"UTC"`. | Additional properties are allowed. ### Schema: Estimate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `ngn` | Yes | number or null | | | `priced` | Yes | boolean | | | `usd` | Yes | number or null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Create an encrypted variable (/api/reference/variables/createServiceVariable) **POST /workspaces/{workspaceId}/services/{serviceId}/variables** Operation ID: `createServiceVariable`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/variables` The stored value is encrypted. The response contains only masked metadata. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [VariableCreate](#schema-variablecreate). Example: ```json { "key": "APP_ENV", "value": "production" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [VariableResponse](#schema-variableresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: VariableCreate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `key` | Yes | string | Maximum length: `255`. Pattern: `^[A-Za-z_][A-Za-z0-9_]*$`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | | `value` | Yes | string | UTF-8 text, at most 65536 bytes. Never returned by CRUD responses. Write only. | ### Schema: VariableResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `variable` | Yes | [Variable](#schema-variable) | | Example: ```json { "variable": { "configured": true, "createdAt": "2026-09-25T12:00:00Z", "id": "00000000-0000-4000-8000-000000000001", "key": "APP_ENV", "kind": "variable", "updatedAt": "2026-09-25T12:00:00Z", "value": null } } ``` ### Schema: Variable Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `configured` | Yes | boolean | Value: `true`. | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `key` | Yes | string | | | `kind` | Yes | string | Value: `"variable"`. | | `updatedAt` | Yes | string | Format: `date-time`. | | `value` | Yes | null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Delete a variable (/api/reference/variables/deleteServiceVariable) **DELETE /workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}** Operation ID: `deleteServiceVariable`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}` Request parameters, responses, and examples for delete a variable. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `variableId` | path | Yes | string | Variable UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Optional. Content type: `application/json`. Schema: [OptionalRequest](#schema-optionalrequest). Example: ```json {} ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [Deleted](#schema-deleted). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: OptionalRequest Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | ### Schema: Deleted Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `deleted` | Yes | boolean | | | `operation` | No | [Operation](#schema-operation) | | | `queued` | No | boolean | | ### Schema: Operation Type: object. Safe job metadata only. Internal payload, results and error details are not included. Tolerate future operation status values. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `kind` | Yes | string | | | `status` | Yes | string | Allowed: `"queued"`, `"running"`, `"complete"`, `"failed"`. | | `updatedAt` | Yes | string | Format: `date-time`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # List masked variable metadata (/api/reference/variables/listServiceVariables) **GET /workspaces/{workspaceId}/services/{serviceId}/variables** Operation ID: `listServiceVariables`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/variables` Values are always null. Legacy requests without limit/cursor return all variables. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `limit` | query | No | integer | Page size. Supplying limit or cursor enables pagination for legacy unbounded lists. Minimum: `1`. Maximum: `200`. Default: `100`. | | `cursor` | query | No | string | Opaque nextCursor returned by the previous page. Reuse with the same endpoint and query; do not parse or construct it. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [VariableList](#schema-variablelist). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: VariableList Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `page` | No | [Page](#schema-page) | | | `variables` | Yes | array of [Variable](#schema-variable) | | ### Schema: Page Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `hasMore` | Yes | boolean | | | `limit` | Yes | integer | Minimum: `1`. Maximum: `200`. | | `nextCursor` | Yes | string or null | | ### Schema: Variable Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `configured` | Yes | boolean | Value: `true`. | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `key` | Yes | string | | | `kind` | Yes | string | Value: `"variable"`. | | `updatedAt` | Yes | string | Format: `date-time`. | | `value` | Yes | null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Reveal a variable with an audit event (/api/reference/variables/revealServiceVariable) **POST /workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}/reveal** Operation ID: `revealServiceVariable`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}/reveal` Developer role or above, plus protected-environment checks. This secret-bearing operation is explicitly excluded from idempotent replay; sending an idempotency key is rejected. Never log this response. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `variableId` | path | Yes | string | Variable UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [RevealedVariable](#schema-revealedvariable). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: RevealedVariable Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `value` | Yes | string | Secret value; do not persist in client diagnostics. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Update a variable name or encrypted value (/api/reference/variables/updateServiceVariable) **PATCH /workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}** Operation ID: `updateServiceVariable`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}/services/{serviceId}/variables/{variableId}` Request parameters, responses, and examples for update a variable name or encrypted value. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `serviceId` | path | Yes | string | Service UUID in this workspace. Format: `uuid`. | | `variableId` | path | Yes | string | Variable UUID in this service. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | | `X-CSRFToken` | header | No | string | Required on browser-session writes: obtain csrfToken from /api/bootstrap and retain its cookies. Not required for a validated bearer key. | | `Idempotency-Key` | header | No | string | Optional retry key of visible ASCII characters without spaces. Successful responses are retained for 24 hours, scoped to workspace, actor, credential, method and path. Changed payload with the same key returns 409 idempotency_conflict. If body requestId is supplied it must match. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | Supported writes accept an `Idempotency-Key`. Reuse the same key and request values for an identical retry within 24 hours. [Retry rules](/api/idempotency). ## Request body Required. Content type: `application/json`. Schema: [VariableUpdate](#schema-variableupdate). Example: ```json { "value": "production" } ``` ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `Idempotency-Replayed` | string | true for a retained response, false for a newly committed keyed operation. Absent when no receipt is used. Allowed: `"true"`, `"false"`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [VariableResponse](#schema-variableresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: VariableUpdate Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `key` | No | string | Maximum length: `255`. Pattern: `^[A-Za-z_][A-Za-z0-9_]*$`. | | `requestId` | No | string | Legacy alias of Idempotency-Key. If both are supplied they must agree. This is distinct from the requestId on an error response. Minimum length: `1`. Maximum length: `160`. Pattern: `^[!-~]+$`. | | `value` | No | string | UTF-8 text, at most 65536 bytes. Never returned by CRUD responses. Write only. | ### Schema: VariableResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `variable` | Yes | [Variable](#schema-variable) | | Example: ```json { "variable": { "configured": true, "createdAt": "2026-09-25T12:00:00Z", "id": "00000000-0000-4000-8000-000000000001", "key": "APP_ENV", "kind": "variable", "updatedAt": "2026-09-25T12:00:00Z", "value": null } } ``` ### Schema: Variable Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `configured` | Yes | boolean | Value: `true`. | | `createdAt` | Yes | string | Format: `date-time`. | | `id` | Yes | string | Format: `uuid`. | | `key` | Yes | string | | | `kind` | Yes | string | Value: `"variable"`. | | `updatedAt` | Yes | string | Format: `date-time`. | | `value` | Yes | null | | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Get a workspace (/api/reference/workspaces/getWorkspace) **GET /workspaces/{workspaceId}** Operation ID: `getWorkspace`. API server: https://api.openstead.tech/api/v1 Request URL: `https://api.openstead.tech/api/v1/workspaces/{workspaceId}` Request parameters, responses, and examples for get a workspace. ## Authentication - **Bearer API key**: send `Authorization: Bearer $OPENSTEAD_API_KEY`. Workspace-scoped rnv_ API key. Read scope allows GET; write scope is additionally constrained by current membership, role, email verification, expiry and workspace MFA policy. - **browserSession**: `runivo_session` in cookie. Authenticated Django browser session. Mutating requests also require the X-CSRFToken header and runivo_csrf cookie from /api/bootstrap. These are alternative authorization methods. Server integrations should use a workspace API key. [Authentication guide](/api/authentication). ## Parameters | Name | In | Required | Type | Description and constraints | | --- | --- | --- | --- | --- | | `workspaceId` | path | Yes | string | Workspace UUID; it must match the bearer key scope. Format: `uuid`. | | `X-Request-ID` | header | No | string | Optional correlation identifier; invalid values are replaced. The accepted/generated value is returned in X-Request-ID and error body requestId. This is not an idempotency key. Maximum length: `64`. Pattern: `^[A-Za-z0-9][A-Za-z0-9._-]*$`. | ## Request body No request body. ## Responses ### HTTP 200 Success. Creates and queued operations intentionally retain HTTP 200. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [WorkspaceResponse](#schema-workspaceresponse). ### HTTP 400 Invalid JSON, values, cursor, confirmation or operation. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 401 Authentication is missing, invalid or expired. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 403 Role, key scope, email verification, MFA or browser CSRF checks failed. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 404 Resource does not exist or is outside the caller workspace membership. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 405 HTTP method is not supported. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 409 Resource conflict, idempotency_conflict, idempotency_legacy_conflict, or engine_unavailable. For a legacy deployment key without a receipt, inspect the existing deployment; do not automatically start another attempt. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 413 Request body exceeds 262144 bytes. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 429 Request rate limit reached; respect Retry-After. | Header | Type | Description and constraints | | --- | --- | --- | | `Retry-After` | integer | Seconds to wait before retrying a rate-limited request. Minimum: `1`. | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ### HTTP 500 Unexpected server failure; use requestId when contacting support. | Header | Type | Description and constraints | | --- | --- | --- | | `X-Request-ID` | string | Correlation identifier for this request. | Content type: `application/json`. Schema: [ErrorResponse](#schema-errorresponse). ## Referenced schemas Only schemas used by this operation are included below. References link to their definitions on this page. ### Schema: WorkspaceResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `workspace` | Yes | [Workspace](#schema-workspace) | | ### Schema: Workspace Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `billing` | Yes | object | Visible fields depend on the caller role. | | `configuration` | Yes | object | | | `configuration.allowOverages` | No | boolean | Default: `false`. | | `configuration.avatarColor` | No | string | Maximum length: `2000`. Default: `"#e6daff"`. | | `configuration.budget` | No | integer | Minimum: `0`. Maximum: `100000000`. Default: `0`. | | `configuration.budgetAlerts` | No | boolean | Default: `true`. | | `configuration.buildConcurrency` | No | integer | Minimum: `1`. Maximum: `10`. Default: `1`. | | `configuration.currency` | No | string | Maximum length: `2000`. Default: `"USD"`. | | `configuration.defaultRegion` | No | string | Maximum length: `2000`. Default: `"auto"`. | | `configuration.deployNotifications` | No | string | Maximum length: `2000`. Default: `"failure"`. | | `configuration.description` | No | string | Maximum length: `2000`. Default: `""`. | | `configuration.includePreviewLogs` | No | boolean | Default: `true`. | | `configuration.notificationEmail` | No | string | Maximum length: `2000`. Default: `""`. | | `configuration.overlappingDeploys` | No | string | Maximum length: `2000`. Default: `"cancel"`. | | `configuration.previewNotifications` | No | boolean | Default: `false`. | | `configuration.requireMfa` | No | boolean | Default: `false`. | | `configuration.securityContact` | No | string | Maximum length: `2000`. Default: `""`. | | `configuration.timezone` | No | string | Maximum length: `2000`. Default: `"Africa/Lagos"`. | | `entitlements` | Yes | object | | | `id` | Yes | string | Format: `uuid`. | | `name` | Yes | string | | | `plan` | Yes | string | | | `role` | Yes | string | Allowed: `"viewer"`, `"developer"`, `"admin"`, `"owner"`. | ### Schema: ErrorResponse Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `errors` | Yes | array of [Error](#schema-error) | Minimum items: `1`. | | `requestId` | Yes | string | Minimum length: `1`. | Example: ```json { "errors": [ { "code": "idempotency_conflict", "message": "This idempotency key was already used with different request values." } ], "requestId": "client-trace-01" } ``` ### Schema: Error Type: object. | Field | Required | Type | Description and constraints | | --- | --- | --- | --- | | `code` | Yes | string | Minimum length: `1`. | | `field` | No | string | | | `message` | Yes | string | | ## Related documentation - [API overview](/api/overview) - [Errors and request IDs](/api/errors) - [Complete OpenAPI specification](/openapi.json) # Promotional credits (/billing/credits) Openstead credits are promotional grants or adjustments attached to a workspace's billing account. They reduce eligible invoice amounts. They are not a general-purpose wallet or cash balance. ## Redeem a code [#redeem-a-code] 1. Switch to the workspace that should receive the credit. 2. Open **Billing → Credit Balance**. 3. Enter the code in **Promo Code** and apply it. 4. Confirm the updated credit balance and history. An owner or admin must redeem the code. A code can be used once per workspace and may have an expiry, redemption limit, and currency restriction. Invalid, expired, exhausted, or previously used codes do not add credit. ## Apply credit to a purchase [#apply-credit-to-a-purchase] When Openstead prepares an eligible invoice, it reserves available credit and shows the deduction in the quote. You pay the remaining amount through checkout. If the credit covers the complete service month, explicitly confirm the purchase. No saved card is necessary. A scheduled renewal still requires confirmation even when its amount due is zero after credit. For example, a $14 service month with $10 available credit has a $4 net amount due, subject to any separately quoted capacity or storage. Review the actual invoice rather than assuming the compute price is the entire total. ## Reserved credit [#reserved-credit] Credit reserved for an unpaid invoice is not simultaneously available for another purchase. Cancelling an eligible unpaid checkout releases its reservation. A payment already submitted to the provider must be reconciled before cancellation can be treated as complete. If a balance appears lower than expected, inspect pending invoices and credit history before redeeming another code or contacting [support](https://openstead.tech/contact). ## Scope and restrictions [#scope-and-restrictions] * Credit belongs to the selected workspace and currency. * USD credit is not automatically converted into historical NGN credit or moved between workspaces. * Purchasing credit top-ups is not the current payment model; purchase the service month directly. * Build-minute packs have their own purchase and minute-grant records. Dollar credit is not itself a build-minute allowance. * A credit balance does not create a service, renew a term, or authorise paid capacity without the required confirmation. If a promotion does not apply as expected, send support the code's name, workspace ID, and invoice number. Never include payment credentials or account passwords. # Invoices and payment records (/billing/invoices) Workspace owners and admins can manage invoices from **Billing → Invoice History**. Records belong to the selected workspace, so check the workspace switcher if an expected invoice is missing. ## Read an invoice [#read-an-invoice] An invoice identifies its service period, currency, status, line items, applied promotional credit, amount due, and recorded payment. Service-month invoices describe the purchased compute and storage capacity. | Field | Meaning | | ----------------- | ------------------------------------------------- | | Subtotal | Quoted charges before promotional credit | | Applied credits | Credit reserved or applied to this invoice | | Amount due | Net amount after applied credit | | Amount paid | Payment recorded against the invoice | | Remaining balance | Amount still unsettled | | Service period | The month covered by the settled service purchase | The accepted price and line items are preserved. The final service period is determined at settlement, including whether a renewal was early or late. ## Pay an open invoice [#pay-an-open-invoice] Open the invoice and select **Pay Invoice**. Review the amount and complete the hosted checkout. If a renewal is fully covered by credit, select **Confirm Renewal with Credits** instead. Remain signed in to the correct workspace when returning. Openstead waits for verified payment confirmation; the success page at the payment provider is not itself a grant of service access. If a payment is pending, allow it to resolve before creating another attempt. If payment failed, inspect the displayed detail and retry through the existing invoice where available. ## Download records [#download-records] Open an invoice to download its PDF or CSV. PDFs are suitable for the human-readable record, while CSV is useful for reviewing line items. A build-minute purchase has its own receipt once payment is confirmed. Store accounting documents according to your organisation's policy. Download links and dashboard access should not be shared with people who are not authorised to see the workspace's billing information. ## Billing details [#billing-details] Update the billing email, company, address, country, and tax identifier under **Billing Information**. Keep these accurate before creating a new invoice. Issued documents preserve their billing profile and historical accounting information. Editing the current profile should not be assumed to rewrite old documents. Contact [support](https://openstead.tech/contact) if a prior invoice needs review. ## Resolve discrepancies [#resolve-discrepancies] Check that you are comparing the same invoice, service period, currency, and purchase. Older usage records and newer prepaid service-month records can have different line-item formats. For a missing or mismatched payment, send support the invoice number and provider reference. Do not send full card details, passwords, or checkout-session secrets. A disputed, refunded, or incorrectly matched payment requires review rather than another automatic charge. # Billing overview (/billing/overview) Openstead charges for paid services one calendar month in advance. Each service has its own plan and renewal date. There is no separate workspace subscription or per-seat surcharge within the included member limit. New payments and billing changes use **US dollars (USD)**. A bank or payment provider may apply its own currency conversion; a naira estimate is not a separate NGN balance or guaranteed exchange rate. The checkout's confirmed currency and amount are authoritative. ## What you pay for [#what-you-pay-for] * The selected compute plan and purchased replica capacity. * Application persistent disks, where configured. * Storage above a database plan's included allocation, when applicable. * Optional build-minute packs purchased separately. Static sites, Free web instances, and MySQL Free do not require a paid service month. See [Free instances](/getting-started/free-instances) and [Pricing](/billing/pricing). ## Purchase a service month [#purchase-a-service-month] An owner or admin selects the instance type, reviews the service quote, and confirms checkout from the dashboard. The quote shows the plan, replicas, storage, any applied promotional credit, and amount due. Openstead sends you to Bachs hosted checkout to complete payment. You do not need to save a card in Openstead. Return to the dashboard after checkout and wait for payment confirmation. If promotional credit covers the full quote, confirm the credit-funded purchase explicitly. A credit balance does not itself authorise starting a paid service. ## Payment and deployment are separate [#payment-and-deployment-are-separate] A completed checkout redirect is not the final proof of payment. Openstead waits for verified payment confirmation before granting the purchased term and queueing provisioning. The service can then be queued, deploying, active, or needing attention. If provisioning fails after payment is confirmed, review the failed operation and use the purchase's retry action. Check the existing invoice and purchase before starting another payment. ## Renew manually [#renew-manually] Openstead issues a renewal invoice two days before the service month ends. You pay it through hosted checkout or explicitly confirm the use of sufficient credit. After expiry, there is a three-day grace period. Unrenewed compute then pauses. Database data and persistent disks are retained; expiry does not automatically delete them. Read [Renewals](/billing/renewals) for early renewal and plan-change timing. ## Workspace benefits from paid services [#workspace-benefits-from-paid-services] A qualifying active paid service unlocks workspace benefits including up to ten members, up to twenty environments per project, private registry configuration, metrics streaming, audit CSV exports, and workspace two-factor enforcement. These benefits depend on purchased, deployed service access. Changing a draft plan selection or retaining an expired purchase does not qualify. Features tied to an individual service, such as an application disk or shell, require that service's own paid instance. ## Manage your billing profile [#manage-your-billing-profile] Open **Billing → Billing Information** to review the billing email, organisation details, and address. Keep the billing email monitored so renewal notices reach the right person. Use [Invoice History](/billing/invoices) for purchase records and downloads, and [Credit Balance](/billing/credits) to redeem promotional codes. Billing actions require an owner or admin in an authenticated browser session. # Plans and pricing (/billing/pricing) Plan prices below are monthly USD prices. Review the dashboard quote before confirming a purchase; it includes the selected capacity and billable storage. Paid service months use [manual checkout and renewal](/billing/renewals). ## Application compute [#application-compute] | Plan | Price per replica / month | Memory | CPU | Pooled build minutes per active application service | Application log history | | -------- | ------------------------- | ------- | --------------- | --------------------------------------------------- | ----------------------- | | Free web | $0 | 512 MiB | 0.1 shared vCPU | Workspace allowance only | 7 days | | Starter | $7 | 512 MiB | 0.25 vCPU | 300 | 7 days | | Builder | $14 | 1 GiB | 0.5 vCPU | 600 | 14 days | | Growth | $28 | 2 GiB | 1 vCPU | 1,200 | 30 days | | Scale | $56 | 4 GiB | 2 vCPU | 2,400 | 30 days | Paid web services, private services, background workers, cron jobs, and Key Value use the paid compute tiers. Free compute is available for web services; private services and background workers are not Free web instances. Static sites have no application compute charge. They still use shared build minutes. Key Value includes 5 GB of persistent database storage and does not contribute application build minutes. Free web instances sleep after inactivity and share a 750-hour workspace allowance each UTC month. See [Free instances](/getting-started/free-instances). ## PostgreSQL and MySQL [#postgresql-and-mysql] | Plan | Price / month | Memory | CPU | Included storage | | ------------------- | ------------- | ------- | ---------------- | ---------------- | | PostgreSQL Starter | $12 | 1 GiB | 0.25 vCPU | 5 GB | | PostgreSQL Standard | $24 | 2 GiB | 0.5 vCPU | 20 GB | | MySQL Free | $0 | 768 MiB | 0.25 shared vCPU | 2 GiB | | MySQL Starter | $12 | 1 GiB | 0.25 vCPU | 5 GB | | MySQL Standard | $24 | 2 GiB | 0.5 vCPU | 20 GB | MySQL Free is limited to one service per workspace and up to 25 simultaneous connections. The allocation includes system files and logs. PostgreSQL and MySQL use their database-specific plans, rather than the application Free plan. ## Persistent storage [#persistent-storage] Application persistent disks cost **$0.25 per GB per month** in addition to compute. A 5 GB disk on one Starter web service therefore produces a base monthly quote of **$8.25**: $7 compute plus $1.25 disk. PostgreSQL and MySQL plans include the storage shown above; Key Value includes 5 GB. Retained allocation above the selected database plan's allowance is quoted separately at $0.25 per GB per month. A downgrade does not shrink a database disk automatically. MySQL Free does not silently grow or bill storage overages. An allocation that cannot fit its cap needs an explicit paid plan or a deliberate data migration. ## Replica capacity [#replica-capacity] Manual replicas multiply the per-replica compute price. Automatic scaling quotes the selected maximum replica capacity. Review that maximum in checkout rather than assuming the smallest currently running replica count is the full purchase price. Persistent local disks and managed databases have scaling restrictions. See [Scaling](/deployments/scaling) before designing a multi-replica service. ## Build minutes [#build-minutes] Each workspace has at least **500 build minutes per UTC calendar month**. Active paid application services contribute their plan allowances to a shared pool; the pool is the greater of 500 or the sum of qualifying contributions. For example, one Starter service's 300-minute contribution leaves the workspace at its 500-minute minimum. Two Builder services contribute 1,200 shared minutes. Databases do not add application build minutes. Additional packs provide **1,000 minutes for $5** and require manual purchase. Purchased packs expire at the end of their UTC purchase month. They are separate from promotional dollar credit and do not renew automatically. One concurrent build is included. Increasing the number of services or buying minutes does not automatically increase build concurrency. ## Team access and currency [#team-access-and-currency] A workspace without qualifying paid services includes one member and two environments per project. A qualifying paid service includes up to ten members and twenty environments per project, with no separate workspace subscription. New checkout is USD-only. Any exchange conversion is determined by the payment provider or your bank. Historical records in another currency remain separate and do not imply an automatic credit conversion. # Renewals and plan changes (/billing/renewals) Paid services renew manually. Openstead does not automatically charge a stored card. Every service displays its own paid-through date and purchase state. ## Renewal timeline [#renewal-timeline] | When | What happens | | ---------------------- | ------------------------------------------------------------------------- | | Two days before expiry | Openstead issues a renewal invoice and sends a notification | | Before expiry | You can pay to extend service from the existing paid-through date | | At expiry | The service enters a three-day grace period if it has not been renewed | | After the grace period | Unrenewed compute pauses; database data and persistent disks are retained | Open **Billing → Invoice History**, review the renewal invoice, and complete its checkout. If reserved promotional credit covers the invoice, use the explicit confirmation action to activate the next month. Receiving an invoice does not mean payment has been taken. Even a fully credit-covered scheduled invoice requires your confirmation. ## Early and late renewal [#early-and-late-renewal] An early renewal extends the existing term. It does not discard the remaining paid days. Calendar-month terms preserve the billing-day anchor where the calendar permits: a term anchored on the 31st uses the last day of a shorter month and can return to the 31st later. A late renewal starts when its payment is confirmed. Openstead does not charge a new term for the unpaid gap. Resuming or provisioning remains a separate operation after confirmed payment. ## Change a plan [#change-a-plan] Review the new instance type and complete its quoted purchase. A plan change purchased during an active month applies at the current term's end and buys the following month. It is not an immediate replacement month with an invented proration credit. The dashboard shows the effective date. Check it before purchasing if you need more capacity urgently; changing a form value does not resize the live service immediately. Only one pending checkout or paid future month is permitted for the service. Resolve an existing payment before creating a conflicting purchase. A pending invoice without an unresolved submitted payment may be replaced when a different plan is selected. ## Storage and downgrades [#storage-and-downgrades] Downgrading compute does not shrink stored database data. The new quote accounts for retained allocation above the smaller plan's included storage. Returning a web service to Free is available after its paid term ends and subject to Free feature restrictions. MySQL can use Free only if its existing allocation fits the 2 GiB cap and a free workspace slot is available. A larger disk is never silently shrunk to fit. ## Pausing is different from deletion [#pausing-is-different-from-deletion] An expired term pauses compute after the grace period. It does not automatically delete your database or persistent disk. Existing database backups still follow their displayed retention dates, and a paused database does not produce fresh scheduled backups. An explicit service-deletion action is separate and can remove resources. Preserve required data and exports before choosing deletion. ## If you paid but the service is not active [#if-you-paid-but-the-service-is-not-active] Inspect the payment and purchase statuses first. A payment may still await confirmation; a confirmed purchase can separately have a provisioning error. Use the retry action for a confirmed purchase that needs provisioning attention. Do not pay the same invoice again merely because the app is not live. Contact [support](https://openstead.tech/contact) with the invoice number, purchase ID, payment reference, and redacted error if the status does not reconcile. # Blueprint examples (/blueprints/examples) These examples use supported Openstead fields. Replace application paths and commands with those in your repository. Validate before applying, and configure secrets separately. ## Python API and static frontend [#python-api-and-static-frontend] This repository has a Django project under `backend` and a static frontend under `frontend`. Both services inherit the Blueprint repository and branch. ```yaml services: - name: example-api type: web plan: free runtime: python rootDir: backend startCommand: gunicorn config.wsgi:application --bind 0.0.0.0:8000 configuration: port: 8000 autoDeploy: commit - name: example-website type: static plan: free runtime: node rootDir: frontend buildCommand: npm ci && npm run build staticPublishPath: dist configuration: autoDeploy: commit ``` Ensure Gunicorn is declared in the backend dependencies and the frontend build really produces `dist`. Set allowed hosts, API origins, and other environment variables in each service. ## Laravel and MySQL [#laravel-and-mysql] ```yaml services: - name: example-laravel type: web plan: starter runtime: php configuration: autoDeploy: commit - name: example-mysql type: mysql plan: mysql-free configuration: sourceType: none databaseVersion: "8.4" databaseName: example_app databaseUser: example_app backupEnabled: true backupRetentionDays: 7 ``` Import configuration with deployment after sync disabled. Deploy MySQL, set Laravel's `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, and `DB_PASSWORD` using the database's private connection details, and configure the application's `APP_KEY` and `APP_URL`. Complete checkout for the web service before its deployment. Configure persistent uploads with a disk through the service's storage controls. This manifest does not add that disk or inject database credentials automatically. ## Worker and scheduled command [#worker-and-scheduled-command] ```yaml services: - name: example-worker type: worker plan: starter runtime: python startCommand: celery -A config worker --loglevel=info - name: example-nightly-report type: cron plan: starter runtime: python startCommand: python manage.py send_daily_report schedule: "0 8 * * *" configuration: timezone: Africa/Lagos timeoutSeconds: 1800 ``` Install the command dependencies in the repository. Configure the queue connection and any application credentials through variables. The scheduled command runs at 08:00 in the configured timezone, with a 30-minute timeout. Both services require paid instance terms. ## Dockerfile application [#dockerfile-application] ```yaml services: - name: example-container type: web plan: free configuration: runtime: docker buildMethod: dockerfile dockerfilePath: Dockerfile dockerContext: . port: 8080 ``` The Dockerfile supplies the startup command. Make the application listen on `0.0.0.0:8080`. A Free service's filesystem is ephemeral; add a paid persistent disk if the application needs durable local writes. ## Deploy a published image [#deploy-a-published-image] ```yaml services: - name: example-image type: web plan: starter configuration: sourceType: image buildMethod: image image: ghcr.io/example/example-api:2026-09 port: 8000 ``` Replace the illustrative image with one you can pull. A private image additionally needs a matching workspace [registry credential](/integrations/container-registries) through `configuration.registryId`. Use a real registry UUID, never the token itself. ## Apply changes safely [#apply-changes-safely] Validate with `openstead blueprints validate --file openstead.yaml` or the dashboard. Review the services and plans created, configure secrets, complete any checkout, and deploy. Confirm the resulting releases individually: a successful Blueprint sync reports applied configuration, while each deployment has its own outcome. # Blueprints (/blueprints/overview) A Blueprint describes services in YAML. Openstead can create their project and Production environment from that file, or synchronize configuration from a connected GitHub repository. Use Blueprints for a monorepo, an application with workers and databases, or a repeatable service layout. Openstead still manages the infrastructure and applies the same permissions, quotas, and payment requirements as individual services. ## Create your first Blueprint [#create-your-first-blueprint] Add `openstead.yaml` to your repository: ```yaml services: - name: website type: static plan: free runtime: node buildCommand: npm ci && npm run build staticPublishPath: dist ``` This example expects a Node-based static site whose build writes `dist`. Adjust the command and output directory to your application. 1. Open **Blueprints → New blueprint** in the dashboard. 2. Give the Blueprint a name and select its repository, branch, and YAML file path. 3. Grant the Openstead GitHub App access to the repository. 4. Choose repository synchronization or paste YAML for manual creation. 5. Validate the configuration and create or synchronize the project. Each service can declare its own `repo` and `branch`. Otherwise repository-backed services inherit the Blueprint repository and, when using that repository, its selected branch. ## Configuration and deployment [#configuration-and-deployment] Creating or synchronizing a Blueprint creates and updates configuration. Deployment is a separate choice. For manual YAML, **Create project from blueprint** creates the project once. Select **Queue deployments after creating the services** to request initial releases. After creation, **Deploy project** deploys the current service settings. Saving manually edited YAML alone does not update existing services. For a repository Blueprint, enable **Deploy new or changed services after syncing** if you want synchronization to queue releases. First create configuration with this option disabled when a paid service needs checkout or application secrets need to be configured. ## Add secrets separately [#add-secrets-separately] Configure service variables and environment groups through the dashboard. Inline `envVars` values are rejected. Openstead does not substitute Render-style secret references or generate secrets from Blueprint YAML. For a database-backed application, deploy the database, obtain its private connection details, configure the application's variables, and then deploy the application. A Blueprint does not automatically wire database credentials into another service. ## Update and remove services [#update-and-remove-services] Names identify service entries. A renamed entry creates a new service and retains the previous service. Removing an entry from YAML does not delete, suspend, or archive the existing service; Openstead reports it as retained so you can decide what to do with it. An existing service's type cannot change in place. Manually moving, renaming, archiving, or deleting a bound service can cause a synchronization conflict that must be resolved explicitly. ## Next steps [#next-steps] * [Repository sync](/blueprints/repository-sync): how pushes update configuration. * [YAML reference](/blueprints/reference): supported fields and limits. * [Examples](/blueprints/examples): monorepos, Laravel and MySQL, workers, and Docker images. # Blueprint YAML reference (/blueprints/reference) Openstead manifests use UTF-8 YAML with a top-level `services` list. They support **1–30 service definitions** and a maximum size of **64 KiB**. YAML anchors and aliases are rejected; repeat each service's settings explicitly. ## Minimal manifest [#minimal-manifest] ```yaml services: - name: example-api type: web plan: free runtime: node startCommand: npm start ``` Every service needs a unique `name`: up to 63 lowercase letters, digits, and hyphens, starting with a letter or digit. Always declare `type` and `plan` explicitly so a manifest communicates its intended resources. ## Service types [#service-types] | `type` | Service | | ---------- | -------------------------------- | | `web` | Public web application or API. | | `static` | Built static site. | | `private` | Internal network service. | | `worker` | Long-running background process. | | `cron` | Scheduled command. | | `postgres` | Managed PostgreSQL. | | `mysql` | Managed MySQL. | | `redis` | Redis-compatible Key Value. | The parser accepts `pserv`, `background_worker`, and `keyvalue` as aliases for `private`, `worker`, and `redis`. Prefer the canonical names above. ## Top-level service fields [#top-level-service-fields] | YAML field | Equivalent service configuration | Meaning | | ------------------- | -------------------------------- | ------------------------------------------------------------------ | | `repo` | `repository` | HTTPS repository URL; otherwise inherited for repository services. | | `branch` | `branch` | Source branch. | | `runtime` | `runtime` | `auto` or a supported runtime. | | `rootDir` | `rootDirectory` | Build root relative to the repository. | | `buildCommand` | `buildCommand` | Build command override. | | `startCommand` | `startCommand` | Command for the running service. | | `preDeployCommand` | `preDeployCommand` | Command before release; paid instance required. | | `staticPublishPath` | `publishDirectory` | Static build output directory. | | `schedule` | `schedule` | Cron expression for a cron service. | | `plan` | `plan` | Valid instance plan for the service kind. | | `configuration` | Configuration mapping | Additional supported service configuration. | When the same setting appears both at the service top level and inside `configuration`, the top-level field wins. Avoid specifying a setting twice. ## Configuration mapping [#configuration-mapping] `configuration` accepts the service configuration fields defined in the [public API specification](https://api.openstead.tech/api/v1/openapi.json). Common fields include: | Field | Values and constraints | | -------------------------------------- | --------------------------------------------------------- | | `sourceType` | `repository`, `image`, or `none`. | | `buildMethod` | `railpack`, `dockerfile`, or `image`. | | `image`, `registryId` | Container reference and optional workspace registry UUID. | | `dockerfilePath`, `dockerContext` | Repository-relative Docker paths. | | `autoDeploy` | `off`, `commit`, or `checks`. | | `port` | Integer from 1 to 65535. | | `healthCheckPath` | Empty or a path starting with `/`. | | `region` | A region ID from the catalog. | | `replicas` | 1–20, subject to service and plan constraints. | | `timezone` | IANA timezone, such as `Africa/Lagos`. | | `timeoutSeconds` | Command timeout; cron maximum is 43200. | | `databaseName`, `databaseUser` | Supported database/user identifiers. | | `databaseVersion` | PostgreSQL `16`, `17`, or `18`; MySQL `8.4`. | | `backupEnabled`, `backupRetentionDays` | Backup settings for supported databases. | Paths cannot contain parent traversal or backslashes. Unknown configuration keys are rejected. Omitted settings use defaults on a new service and retain existing values during synchronization. `runtime: auto` allows build detection. Other runtime values are `docker`, `node`, `python`, `go`, `php`, `java`, `ruby`, `rust`, `elixir`, `deno`, `dotnet`, `gleam`, `cpp`, `static`, and `shell`. Use Docker when your application needs dependencies or startup behavior outside an automatic build. ## Plans [#plans] Application plans are `free`, `starter`, `builder`, `growth`, and `scale`. Free application instances are limited to web services and static sites, with a single replica and no paid-only settings. PostgreSQL uses `postgres-starter` or `postgres-standard`. MySQL uses `mysql-free`, `mysql-starter`, or `mysql-standard`. MySQL Free is limited to one database per workspace. Read the catalog and current pricing before deploying a manifest containing paid resources. Managed MySQL uses one private instance on port 3306 with `sourceType: none`. Repository build commands, HTTP domains, and auto-deploy settings do not apply to it. MySQL Free backup retention is 1–7 days; other database plans accept 1–30 days. ## Secrets and other resources [#secrets-and-other-resources] Inline `envVars` values are rejected. An `envVars` entry without a value does not create or wire a variable. Use service variables or environment groups after configuration is created. Render-specific directives such as `fromDatabase`, `generateValue`, `sync`, and top-level `databases` are not Openstead manifest features. Define databases inside `services`; configure variables, persistent disks, custom domains, and other service resources through their dashboard controls. ## Validate [#validate] Use the dashboard's validation control or the CLI: ```bash openstead blueprints validate --file openstead.yaml ``` Validation checks the manifest without provisioning resources. Applying or deploying still checks workspace quotas, source access, current entitlements, and service payment requirements. # Sync Blueprints from GitHub (/blueprints/repository-sync) Repository synchronization connects a saved Blueprint to one GitHub repository, branch, and YAML path. A workspace owner or admin authorizes automatic sync. ## Connect the source [#connect-the-source] 1. Install or configure the Openstead GitHub App for the workspace and grant access to the Blueprint repository. This is required even for a public repository. 2. Open the Blueprint and enter its repository URL. 3. Select the branch and YAML path. Defaults are `main` and `openstead.yaml`. 4. Enable **Sync YAML automatically from GitHub** and save. 5. Check the imported manifest and resulting services. You can save a repository draft without pasting YAML. Use the page's GitHub connection controls if access needs updating; completing authorization returns you to Blueprints. Existing Blueprints keep their configured YAML path. If you rename a manifest to `openstead.yaml`, update the Blueprint's path to match. ## Decide when to deploy [#decide-when-to-deploy] **Deploy new or changed services after syncing** queues releases for synchronized changes. Paid terms must already cover the requested capacity. If admission fails, the whole configuration synchronization rolls back. For a new paid application, first sync with deployment disabled. Complete checkout and configure secrets, then deploy the services and enable deployment after sync if appropriate. Each service can also use `configuration.autoDeploy: commit` for ordinary code pushes. A release already queued by Blueprint synchronization is not duplicated by the regular push handler for the same commit and configuration. ## What triggers a sync [#what-triggers-a-sync] Changes to the selected YAML path on the selected branch trigger synchronization. Events for other branches, deleted branches, unrelated installations, and pull requests do not apply that YAML. The worker reads the file from an immutable commit and applies validated changes together. Existing valid configuration remains available if importing or validation fails. Temporary GitHub failures retry up to five attempts. Use **Sync now** to import the current branch immediately or retry after fixing a problem. It works with automatic sync enabled or disabled. A newer synchronization request supersedes older pending work. ## Understand the status [#understand-the-status] The Blueprint page displays synchronization progress, the last applied commit, last sync time, errors, and retained service names. `synced` means the configuration was applied. Inspect each service's deployment to confirm it became live. While automatic sync is enabled, edit the YAML in GitHub. The dashboard prevents editing an unimported local draft as though it were the repository source. ## Authority and access [#authority-and-access] The owner or admin who enabled synchronization must retain active, verified workspace access and satisfy its MFA policy. GitHub App access is checked again when reading the repository. If authority changes, another owner or admin can reconnect sync or use **Sync now**. Disabling automatic synchronization invalidates pending sync work without deleting the Blueprint's services. ## Resolve common failures [#resolve-common-failures] | Failure | Resolution | | ------------------------------ | --------------------------------------------------------------------------- | | Repository access denied | Grant the connected Openstead GitHub App access to the selected repository. | | YAML file not found | Check branch, filename, case, and path relative to the repository root. | | Invalid configuration | Validate against the [YAML reference](/blueprints/reference). | | Paid deployment rejected | Sync without deployment, complete service checkout, then deploy. | | Bound service changed manually | Resolve its move, rename, archive, or deletion before syncing again. | | Service name already in use | Choose a unique name or resolve the existing service explicitly. | | Owner/admin access required | Reconnect synchronization with a current authorized workspace account. | Removing a service from YAML retains it. Delete a retained service through the project only when you intend to remove that resource and its data. # From repository to running application (/changelog/2026-09-21) Openstead started with a straightforward idea: bring your application code and configuration, and let us operate the infrastructure. Product planning began on September 23. The releases collected in this first weekly update followed on September 24–26. ## Added [#added] * **Deploy from GitHub or a container image.** Connect a repository, let Openstead detect the application, review the build settings, and deploy a [web service](/services/web-services), [static site](/services/static-sites), [private service](/services/private-services), [background worker](/services/background-workers), or [cron job](/services/cron-jobs). Dockerfile and image-based deployments support applications that need their own build or runtime setup. * **Managed databases alongside your applications.** [PostgreSQL](/databases/postgresql), [MySQL 8.4](/databases/mysql), and Redis-compatible [Key Value](/databases/key-value) connect to services over Openstead's private network. PostgreSQL and MySQL include backup and separate-database restoration workflows. * **MySQL Free.** Each workspace can run one free MySQL database with 768 MiB of memory, 2 GiB of persistent storage, up to 25 connections, and up to seven days of backups. It does not require a card or expire automatically. [Explore MySQL plans](/databases/mysql). * **phpMyAdmin from the console.** Open [phpMyAdmin](/databases/phpmyadmin) from an eligible MySQL service using your Openstead session. The manager is limited to that database, while the MySQL connection itself remains private. * **Projects, environments, and application configuration.** Group services into [projects and environments](/account/projects), manage encrypted [variables and secret files](/deployments/environment-variables), and configure private service connections, persistent disks, and custom domains with HTTPS. * **Release and runtime controls.** Follow deployment stages, inspect [logs and metrics](/deployments/logs), configure health checks, and [roll back](/deployments/rollbacks) to a retained release. Restart, suspend, and resume services from the console. Paid application instances also support the shell and one-off commands. * **Blueprints with GitHub synchronization.** Define related services in a Blueprint YAML file and create their project together. Repository synchronization applies configuration changes from the selected branch, with an option to deploy changed services. Removed YAML entries retain their existing services until you explicitly remove them. [Use Blueprints](/blueprints/overview). * **A documented core API and developer tools.** The [REST API](/api/overview) includes an OpenAPI contract, bounded collection pagination, structured errors, and idempotent writes. [Python](/integrations/python-sdk) and [TypeScript](/integrations/typescript-sdk) clients add typed responses, iterators, and deployment polling; their guides explain repository access and installation. The [CLI](/integrations/cli) and [MCP server](/integrations/mcp) provide workspace-scoped access from terminals and compatible assistants. * **Connections to your existing tools.** Deploy private images with workspace [registry credentials](/integrations/container-registries), receive signed [webhook events](/integrations/webhooks), send Slack or Discord notifications, and forward application logs or metrics to your own collector. * **Monthly service checkout and renewal invoices.** Paid applications and databases use hosted checkout for monthly service terms. [Renewal invoices](/billing/renewals) are issued two days before expiry. An unpaid term has a three-day grace period before compute pauses, with database data and disks retained. ## Improved [#improved] * **Less repeated deployment work.** Build preparation now warms the tools used by the actual build, service-specific caches retain reusable layers, and matching unchanged release artifacts can be reused. Eligible static applications publish their built files directly. [How builds work](/deployments/builds). * **Clearer custom-domain progress.** The domain dialog checks verification automatically while open and shows DNS verification and HTTPS readiness separately. [Connect a domain](/networking/custom-domains). * **A more direct sign-in experience.** Social sign-in buttons show the last successfully used provider. Full-page preloaders were removed from authentication and dashboard interfaces so the page structure can appear while data loads. ## Fixed [#fixed] * **PostgreSQL persistence across replacement.** Corrected version-specific data mounts and added storage-identity checks. Replacement and backup-restoration checks confirmed data remains on the intended persistent volume. [Database storage and recovery](/databases/backups). * **Repository access for GitHub organizations.** Configuration links now use the correct organization installation page instead of a personal-account URL that can return 404. [Manage GitHub access](/deployments/github). * **Repeated environment keys during service creation.** Environment-file imports keep the last value for each key. Duplicate keys entered directly receive a specific validation message instead of the misleading resource-name conflict. [Environment variables](/deployments/environment-variables). * **Cancellation and intentional stops.** Queued cancellations finish without being reported as operator failures, and intentional service stops no longer trigger the same outage alerts as an unexpected failure. # Documentation, connected navigation, and clearer errors (/changelog/2026-09-28) This week's updates make Openstead easier to learn and navigate, with a dedicated documentation site and clearer information when something goes wrong. This entry includes changes released on September 28. ## Added [#added] * **A dedicated documentation site.** The initial release brings together 109 pages covering services, frameworks, private databases, networking, Blueprints, developer tools, workspaces, and billing. Start with your [first deployment](/getting-started/first-deploy), choose a [service type](/services/overview), or jump into a framework guide such as [Laravel](/guides/laravel), [Django](/guides/django), or [Next.js](/guides/nextjs). * **An API reference for all 28 core operations.** Each [endpoint page](/api/reference) includes its method, request fields, responses, and code examples. The interactive playground sends requests to the real Openstead API, with a workspace key for authenticated operations. Review the operation before sending a request that changes a resource. * **Search and Markdown access.** Search across guides and reference pages, use the page outline to move through longer articles, or copy a page as Markdown. The [documentation index](/llms.txt) and [complete Markdown export](/llms-full.txt) make the same material available to developer tools, including useful endpoint details and referenced schemas. ## Improved [#improved] * **Documentation where you need it.** The marketing website and console help now link to the hosted documentation. Product pages lead to relevant guides, and existing documentation links redirect to the new site. * **Website navigation for signed-in users.** After your console session is confirmed, the marketing header shows **Dashboard** in place of **Log in** and **Sign up**. Session checks happen in the background without delaying the page. * **Clearer service and pricing information.** Website content now explains the services offered in the console, resource pricing, and monthly renewal behavior. USD checkout and illustrative NGN budget estimates are distinguished, with persistent-storage costs included in the calculator. * **Consistent platform failure pages.** Missing-page and runtime failures across the website and console use a shared presentation. Hosted gateway failures show a clear status and next step; when a request reference is available, it can help support identify that failure. Your application's own responses and custom error pages remain under your control. [Troubleshoot a deployment](/guides/troubleshooting). ## Fixed [#fixed] * **Static-site platform fallbacks.** Corrected the routing used to serve platform error responses for static deployments, while retaining custom `404.html` pages and SPA routing behavior. * **Usable API examples from the first render.** Reference examples use the Openstead API URL and clearly marked credential placeholders. Markdown copies include endpoint parameters, request and response information, rather than an empty component placeholder. # Changelog Weekly additions, improvements, and fixes across Openstead. ## Week of September 28, 2026 [Documentation, connected navigation, and clearer errors](/changelog/2026-09-28) Find guides and API answers in one place, return to your dashboard from the website, and get clearer information when a request fails. - A dedicated documentation site - All 28 core API operations documented - Dashboard links for confirmed sessions - Clearer platform failure pages Updated September 28, 2026. ## September 23–27, 2026 [From repository to running application](/changelog/2026-09-21) Deploy applications and managed databases, define services in YAML, and work from the dashboard, API, or CLI. - GitHub deployments and release controls - MySQL Free and secure phpMyAdmin access - Blueprints, API, SDKs, and CLI - Monthly checkout with renewal invoices Updated September 28, 2026. [Subscribe with RSS](/changelog/feed.xml) # Backups and recovery (/databases/backups) Openstead supports logical backups for PostgreSQL, MySQL, and Key Value. Manage them from a database service's **Recovery** page. Backup management requires an owner or admin. ## Automatic backups [#automatic-backups] Daily backups are enabled by default. In **Automatic Backups**, choose whether daily backups run and set retention: | Database plan | Retention choices | | ------------------------------------ | ----------------- | | MySQL Free | 1–7 days | | Paid PostgreSQL, MySQL, or Key Value | 1–30 days | The retention setting applies to new exports. Existing exports keep their displayed expiry dates. Scheduled backups require the database to be available; a paused or unavailable database does not produce a new successful recovery point. Regularly check for completed backups. An enabled schedule alone does not prove that the latest export succeeded. ## Create an export now [#create-an-export-now] 1. Open the database's **Recovery** page. 2. Select **Create export**. 3. Wait for the export to complete and check its creation time and size. 4. Use the download action when you need a local copy. The download link is short-lived. If it expires, request another from the dashboard. Keep downloaded files in protected storage. | Database | Export format | | ---------- | --------------------- | | PostgreSQL | Custom-format `.dump` | | MySQL | Logical `.sql` | | Key Value | `.rdb` | MySQL's consistent export path requires InnoDB tables. Avoid schema changes during the dump. Backups include the application's schema and data; they do not copy root credentials or the full MySQL server's system databases. ## Restore a recovery copy [#restore-a-recovery-copy] Restoration always creates a **separate database**. It does not overwrite the source. 1. Choose a completed, unexpired export in Recovery. 2. Select **Restore into a new database**. 3. Name the recovery database, choose its plan, and confirm the source name. 4. If the selected plan is paid, review and complete checkout for the recovery instance. 5. Wait for provisioning and the actual import to finish. 6. Verify representative tables, rows, and application behaviour before switching traffic. A paid recovery instance has its own service month. For MySQL Free, the destination needs an available free slot. If the source occupies that slot, choose a paid recovery instance explicitly; Openstead does not silently purchase one. ## Switch applications after verification [#switch-applications-after-verification] Change each application's connection references to the restored database and redeploy. Include workers and scheduled processes that also access the data. For an active application, plan a final write pause or another reconciliation method so writes made after the backup are not lost during cutover. A logical recovery point contains the state captured by that export, not every later transaction. New recovery instances have automatic backups disabled initially. After accepting the recovered database, review and enable the desired backup policy on the destination. Retain the original database until you are satisfied with the cutover. ## Retry a failed restore [#retry-a-failed-restore] The recovery page preserves the failed result and offers a retry into the same destination. Use this flow instead of repeatedly creating databases. It retains the selected archive and existing destination rather than purchasing another recovery instance. Check the original error first. MySQL imports can leave partial data after a failure; let the managed retry workflow handle its recovery destination. Do not point applications at a database still marked as restoring or needing attention. ## What these backups cover [#what-these-backups-cover] Logical database backups do not include application uploads, general-purpose persistent disks, or the application source repository. They are not whole-server images or continuous point-in-time recovery. A persistent volume protects against routine container replacement. A backup provides a separate logical recovery point. Neither should be described as automatic high availability. Test restoration before relying on it for a critical recovery procedure. Deleting an export removes that recovery point. Openstead prevents deletion of an export reserved by an unfinished restore until the restore is resolved or the recovery instance is explicitly deleted. # Database connections (/databases/connections) Managed databases are private services. Place the application and database in the same Openstead environment and connect with the values shown in the database's **Connections** page. ## Prefer connection references [#prefer-connection-references] A connection reference maps an environment variable on an application to a field on a database. Openstead resolves it when creating a release, so the variable listing does not need to expose the database password. Open the application's **Environment** page and find **Connected Services → Connect service**. Choose the database from the available sources, enter the variable name, and select its connection field. Then deploy the application. | Database | Suggested variable | Field | | ---------- | ---------------------------------------- | ------------------------------------ | | PostgreSQL | `DATABASE_URL` | Connection URL | | MySQL | `DB_URL`, or individual `DB_*` variables | Connection URL, or individual fields | | Key Value | `REDIS_URL` | Connection URL | Use the variable names your framework actually reads. Laravel commonly uses separate MySQL values; see [MySQL](/databases/mysql). An ordinary environment variable or linked environment-group variable cannot use the same name as a connection reference. Remove or rename the conflicting variable first. ## What is available [#what-is-available] | Field | PostgreSQL | MySQL | Key Value | | -------------- | ---------------- | ---------------- | --------------------------------- | | Host | Private hostname | Private hostname | Private hostname | | Port | `5432` | `3306` | `6379` | | Username | Yes | Yes | Not a separate field | | Password | Yes | Yes | Yes | | Database name | Yes | Yes | Use the database index in the URL | | Connection URL | Yes | Yes | Yes | The source database must be running and its restore must have completed. Openstead blocks a dependent deployment if its reference cannot be resolved safely. ## Reveal credentials when necessary [#reveal-credentials-when-necessary] Authorised members can select **Reveal connection credentials** on the database's Connections page. Copy only the fields required by the client and keep them in a secrets manager. Do not commit connection URLs, place them in public frontend variables, paste them into support messages, or print them in build logs. A URL contains credentials even if it looks like a normal address. Use the supplied URL rather than manually concatenating a password into a URL. Special characters must be URL-encoded correctly; the generated URL handles this. ## When changes take effect [#when-changes-take-effect] Connection values are saved into each release's environment. Adding or removing a reference changes future deployments; it does not rewrite an already running process. After changing a reference, switching to a restored database, or changing an address, redeploy every affected web service and worker. Existing releases retain their previous configuration until replaced. Moving a database or application to another environment can make a reference invalid. Keep dependent services together or create an appropriate database in the destination environment. ## Access from your computer [#access-from-your-computer] The private hostnames do not resolve as public database endpoints. Desktop tools cannot connect directly over the internet. MySQL's [phpMyAdmin](/databases/phpmyadmin) provides authenticated browser access without publishing port 3306. For a command-line operation, use a suitable client installed in an authorised application in the same environment, where [shell access](/deployments/shell) is supported. Managed database services themselves do not provide an interactive shell or arbitrary one-off commands. ## Connection troubleshooting [#connection-troubleshooting] Check the environment first, then the service's live state and credentials. If those are correct, inspect client driver compatibility, connection-pool limits, and the application's actual environment after deployment. A build process should not assume it can contact a runtime-only private database; run schema changes at the appropriate deployment or runtime stage. # Key Value (/databases/key-value) Openstead Key Value provides a Redis-compatible service for caches, background-job queues, rate limits, and shared application state. It runs separately from your web application and connects through the private network. ## Create a Key Value service [#create-a-key-value-service] 1. In your application's project and environment, select **New → Key Value**. 2. Choose a service name and a paid instance type. 3. Select the eviction policy appropriate to the workload. 4. Review the service-month quote and complete checkout. 5. Wait for the service to be running, then open **Connections**. Key Value uses the paid application compute tiers and includes 5 GB of persistent database storage. It does not use the MySQL Free plan. See [Pricing](/billing/pricing). ## Connect with a client [#connect-with-a-client] Connect the service's **Connection URL** field to `REDIS_URL` on your application, then redeploy. The private port is `6379`. The URL contains the authentication password; treat it as a secret. For Python with the `redis` package: ```python import os import redis client = redis.Redis.from_url(os.environ["REDIS_URL"], decode_responses=True) client.set("example:message", "Hello from Openstead", ex=60) print(client.get("example:message")) ``` Use connection pooling rather than opening a new connection for every command. Select client timeouts and retry behaviour that suit your request latency and queue semantics. The connection is for applications in the same environment. A browser, laptop, or service in another environment cannot use the private hostname. Key Value has no public endpoint or phpMyAdmin interface. ## Choose an eviction policy [#choose-an-eviction-policy] | Policy | Behaviour | | -------------- | ---------------------------------------------------------------- | | `allkeys-lru` | Prefers evicting less recently used keys | | `allkeys-lfu` | Prefers evicting less frequently used keys | | `volatile-lru` | Evicts less recently used keys only when they have an expiry | | `noeviction` | Refuses writes that require more memory instead of evicting keys | An eviction policy controls Redis behaviour when its configured memory threshold is reached; it is not a guarantee that a process can never exhaust its instance memory. Monitor resource use and leave room for Redis overhead, persistence, and client buffers. For caches, set expirations and design the application to rebuild missing values. For durable job queues, review the queue library's recommended policy; silently evicting queue keys can lose work. `noeviction` requires monitoring and handling write errors when capacity is exhausted. ## Persistence and recovery [#persistence-and-recovery] Openstead enables append-only persistence and stores the data directory on a persistent volume. Logical backups produce `.rdb` exports and can be managed in **Recovery**. See [Backups](/databases/backups). A restored queue may contain work that was already processed after the recovery point. Design consumers to be idempotent, and pause producers or consumers during a recovery cutover when required. Persistent storage and backups do not provide an automatic high-availability cluster or continuous point-in-time recovery. Use your application's source-of-truth database for data that should not depend solely on cache availability. ## Common issues [#common-issues] * **Authentication error:** use the complete current connection reference, including its password. * **Connection refused:** check service state and that the client is in the same environment. * **Missing cache entries:** inspect TTLs and eviction behaviour before assuming a storage failure. * **Unexpected queue behaviour after restore:** reconcile queued jobs with the application database and avoid replaying non-idempotent operations blindly. # Migrate an existing database (/databases/migrate) Treat a database migration as a data-transfer project: preserve the source, import into a separate destination, verify it, and then switch the application. Do not delete a working source as the first step. ## Prepare the migration [#prepare-the-migration] * Record the source engine and version, database size, storage engine, collation, and required extensions. * Export both the database and any application files stored outside it. * Preserve the application's encryption key and required secrets; replacing an encryption key can make existing encrypted values unreadable. * Choose an Openstead destination plan with room for indexes, logs, temporary work, and future growth. * Decide how to stop or reconcile writes during the final cutover. Openstead MySQL uses version 8.4. A MariaDB dump or older MySQL application may need syntax, collation, or SQL-mode changes. PostgreSQL extensions and major-version compatibility should also be checked before import. ## Create an empty destination [#create-an-empty-destination] Create the managed database in the application's intended environment. Wait for it to be running, and record its connection fields securely. The database ports are private. A client on your laptop cannot import directly into the private hostname. Use phpMyAdmin for suitable MySQL imports or a deliberate migration client running within the same Openstead environment. ## Import MySQL [#import-mysql] For smaller SQL files, use [phpMyAdmin](/databases/phpmyadmin). Its total request ceiling is 32 MiB and execution limit is 60 seconds, so large or complex imports need another approach. A migration client in an authorised application shell can use the standard MySQL client if that binary is installed in the application's image: ```bash mysql --host="$DB_HOST" --port="$DB_PORT" \ --user="$DB_USERNAME" --password "$DB_DATABASE" < backup.sql ``` The `--password` option prompts for the password instead of embedding it in the command history. Substitute your current private connection values through the application's environment. Transfer the dump through a secure, controlled path and delete temporary copies after verification. Inspect dumps for `CREATE DATABASE`, `USE`, `GRANT`, and `DEFINER` statements that refer to the old host's database or users. The managed application user cannot create arbitrary server users or access other schemas. Do not import MySQL system databases. Use InnoDB for tables that need Openstead's consistent logical-backup workflow. If an import fails, check the partial destination before retrying rather than assuming every statement rolled back. ## Import PostgreSQL [#import-postgresql] Use PostgreSQL client tools compatible with the dump version from an authorised application environment. A custom-format archive uses `pg_restore`; a plain SQL dump uses `psql`. ```bash pg_restore --no-owner --no-acl --single-transaction \ --dbname="$DATABASE_URL" backup.dump ``` This example assumes a new empty database, a compatible archive, and an installed PostgreSQL client. Review extensions and privileges before running it. Do not run a destructive clean option against a database already serving customers. The dashboard's Recovery workflow restores Openstead-created backups into separate managed instances. It is not an arbitrary file-upload importer for every external dump format. ## Verify before switching [#verify-before-switching] Compare expected table counts and representative rows. Test login, encrypted application fields, reports, queues, migrations, and file references. Confirm the application uses the destination's database name and current credentials. For a final cutover, pause source writes as required, take the final export, complete the import, and point application references to the destination. Redeploy every process that uses the database, including workers. Create a fresh Openstead backup and test application behaviour before retiring the source. Retain the original export according to your recovery requirements. ## If migration needs assistance [#if-migration-needs-assistance] Contact [Openstead support](https://openstead.tech/contact) with the engine versions, approximate dump size, destination service ID, and redacted error text. Never send database passwords, complete connection URLs, or production dumps containing customer data. # MySQL (/databases/mysql) Openstead MySQL runs MySQL 8.4 LTS with persistent data storage, private application connections, logical backups, and access through phpMyAdmin. Each service has its own database credentials. ## Create a database [#create-a-database] Choose **New → MySQL** inside the application's project and environment. Set a service name, database name, and database username. MySQL Free is selected by default; choose a paid MySQL plan if you need more resources. A Free database deploys without a card or checkout. A paid database requires confirmation of the service-month quote. Wait until provisioning and any restore have completed before connecting an application. ## MySQL Free [#mysql-free] | Resource | Included | | ------------------------------ | -------------------------------------------- | | Price | $0 | | MySQL version | 8.4 LTS | | RAM | 768 MiB | | CPU | 0.25 shared vCPU | | Persistent allocation | 2 GiB, including system files and logs | | Simultaneous connections | Up to 25 | | Workspace limit | One MySQL Free database | | Backups | Daily by default; retention from 1 to 7 days | | Idle sleep or scheduled expiry | None | Free is useful for small applications, prototypes, and learning. The storage limit does not automatically expand, and reaching it does not create overage charges. Clean up unneeded data or deliberately upgrade before the allocation fills. Pending, suspended, and archived Free databases retain the workspace's free slot. Deleting a database releases its slot only after deletion finishes. A paid upgrade releases the slot once the paid release is running. Editing a plan selection alone does not release it. A second independent MySQL database in the same workspace needs a paid plan. This also applies when restoring a Free database into a separate recovery database while the source occupies the free slot. ## Connect Laravel or another application [#connect-laravel-or-another-application] MySQL listens privately on port `3306`. Open **Connections** to reveal credentials, or add connection references to the application. For Laravel, set `DB_CONNECTION=mysql`, then map these reference fields: | Application variable | MySQL connection field | | -------------------- | ---------------------- | | `DB_HOST` | Private hostname | | `DB_PORT` | Port | | `DB_DATABASE` | Database | | `DB_USERNAME` | Username | | `DB_PASSWORD` | Password | Redeploy the application after adding or changing references. A copied cPanel value such as `DB_HOST=127.0.0.1` points to the application container itself and will not reach your separate managed database. Use the database's application credentials. Openstead does not expose a MySQL root account for customer login. The credentials are scoped to this database. For a complete deployment, follow [Laravel](/guides/laravel). ## Manage tables with phpMyAdmin [#manage-tables-with-phpmyadmin] Select **Connections → Open phpMyAdmin**. It opens an authenticated database manager in a separate tab, included with both Free and paid MySQL. Your database stays private. See [phpMyAdmin](/databases/phpmyadmin) for SQL imports, session expiry, upload limits, and access permissions. ## Storage, backups, and upgrades [#storage-backups-and-upgrades] MySQL data lives on managed persistent storage and survives normal container replacement. Keep database identity and version changes separate from routine application redeployments. Daily backups use logical SQL exports. The consistent backup path requires InnoDB tables. Avoid schema changes while an export is running. Exports do not include your application's upload directory. Paid plans include larger storage allocations; upgrades grow the managed disk. Openstead does not shrink a disk when you downgrade. An existing allocation above 2 GiB cannot switch to Free. To use Free with a smaller dataset, export and migrate deliberately to a new database that fits. ## Troubleshooting [#troubleshooting] **Connection limit reached:** reduce each application's pool size and count every replica, worker, and manager session. MySQL Free allows up to 25 simultaneous connections across clients. **Storage full:** inspect table sizes and log/data growth. Remove data only when it is safe to do so, retain an export, and review a paid plan if the application has outgrown Free. **Backup rejects a table:** check its storage engine. Migrate nontransactional tables to InnoDB after verifying application compatibility. **phpMyAdmin is unavailable:** confirm the database is running, restoration is finished, your account has permission, and any paid term is active. Reopen the manager after database replacement or session expiry. # Persistent disks (/databases/persistent-disks) An application's writable container filesystem is temporary. Use a persistent disk when files must survive normal container replacement, such as uploaded images, generated documents, or application-managed data files. Managed PostgreSQL, MySQL, and Key Value services configure their own database storage. You do not need to add a general-purpose application disk to those services. ## Supported services [#supported-services] Persistent application disks are available on paid web services, private services, and background workers. Each service can configure one disk. Static sites, Free web instances, and cron jobs do not support this application-disk resource. The application-disk configuration supports 5–1,000 GB. Disk capacity costs **$0.25 per GB per month** in addition to application compute. Review the complete quote before purchasing or changing billable capacity. ## Add a disk [#add-a-disk] Open the service's **Disk** page, add a disk name, choose its size, and enter an absolute mount path such as `/var/data`. Review any billing requirement and deploy to apply the mount. Configure the application to write durable files inside that exact path. Files outside it remain temporary. ```text /var/data/ uploads/ generated-reports/ ``` Do not mount over the entire application source directory or a system path. A volume mounted over a directory hides the image's files at that location, which can make application code or packaged assets appear missing. ## Migrate existing files [#migrate-existing-files] Adding a disk does not automatically copy files from an old container, a cPanel account, or your computer. Export and preserve those files first, attach the disk at the intended path, and perform a controlled import. For Laravel applications, account for the framework's storage paths and public storage link. See [Laravel](/guides/laravel) for application-specific guidance. Preserve existing application encryption keys when migrating encrypted data. ## Deployment and scaling behaviour [#deployment-and-scaling-behaviour] A disk belongs to a service; it is not shared storage that multiple independent services can mount. Services using persistent local storage cannot be treated as stateless, freely interchangeable replicas. Keep their replica configuration within Openstead's disk restrictions. Changing a mount path can make existing files inaccessible to the application even though the data still exists at its original location. Plan path changes as a data migration and back up first. Increasing a disk's allocation is different from moving or shrinking its data. Do not assume a plan downgrade shrinks a volume or deletes files. Review the current allocation and accepted quote. ## Back up application files separately [#back-up-application-files-separately] Openstead's managed database exports contain database data. They do not back up this upload directory. Arrange a separate export or copy process for important application files and test that it can restore them. For applications that need shared uploads across many replicas, consider an external object-storage service and configure the application to use it. Do not rely on a single local disk as a shared multi-service object store. ## Troubleshooting [#troubleshooting] **Files disappear after deployment:** verify the application writes under the disk mount, rather than a similarly named directory inside the image. **Permission denied:** check the application user and filesystem permissions. Avoid making sensitive files world-writable as a quick fix. **Application code is missing:** check whether the mount covers a directory containing code from the image. Use a dedicated data path and migrate carefully. **A file upload exceeds capacity:** inspect actual disk usage, remove data only after preserving what you need, and review a larger allocation. # phpMyAdmin (/databases/phpmyadmin) phpMyAdmin gives you a browser interface for your managed MySQL database. It is included with MySQL Free and paid MySQL plans. Opening the manager does not create another billable application. ## Open the database manager [#open-the-database-manager] 1. Sign in to the Openstead dashboard. 2. Open the MySQL service and select **Connections**. 3. Select **Open phpMyAdmin**. 4. Allow a new tab if your browser blocks pop-ups. The first launch prepares the manager and can take a little time. Keep the dashboard open while it checks readiness. If preparation takes longer than the displayed wait, try opening it again. You do not need to enter a database password into a separate login form. Openstead authorises the handoff using your current browser session. ## Who can open it [#who-can-open-it] Owners, admins, and developers can open phpMyAdmin. A database in a protected environment requires an owner or admin. Viewers cannot open the manager. The database must be running, any restore must be complete, and paid plan access must be valid. Your email must be verified and your account must satisfy the workspace's two-factor authentication requirement. ## Browse and query [#browse-and-query] Select the application database to inspect its tables. Use **Browse** to review rows and **SQL** to execute a query, for example: ```sql SELECT DATABASE(), VERSION(); ``` The manager uses the application's database user. It does not grant root access, access to other customers' databases, or a choice of arbitrary database servers. SQL changes affect the live application immediately. Create a [backup](/databases/backups) before deleting records, changing table structure, or importing a dump over existing tables. ## Import a SQL file [#import-a-sql-file] Select the target database, open **Import**, choose the SQL file, and review the import options before starting. For a migration, use a new empty database and inspect the dump for database-selection statements, user grants, and version-specific syntax. The manager has a **32 MiB total upload/request ceiling**, including form overhead, and a **60-second execution/input time limit**. A file close to 32 MiB can exceed the request ceiling. Larger imports need a deliberate migration using a client in the private environment; see [Database migrations](/databases/migrate). An interrupted SQL import can leave partially imported objects. Review the destination before retrying. MySQL schema operations do not behave like one transaction that can always be rolled back. ## Export data [#export-data] Use phpMyAdmin's **Export** feature for an interactive SQL export. Downloaded data is sensitive and should be stored securely. A phpMyAdmin export is separate from Openstead's scheduled backups and their retention policy. Use **Recovery → Create export** when you want an export recorded in Openstead's managed backup history. ## Session lifetime [#session-lifetime] A manager session expires after 30 minutes. An inactive manager is removed after 15 minutes without activity across its sessions. Open it again from the dashboard when you need more time. Signing out of Openstead, revoking the original browser session, losing workspace permission, or replacing the database invalidates access. Opening the manager again from the same browser replaces that browser's previous manager session for this database. Closing a tab is not an immediate logout. Use account-session controls when you need to revoke access. Manager expiry or removal does not delete the MySQL database. ## Troubleshooting [#troubleshooting] | Issue | Action | | ----------------------------- | -------------------------------------------------------------------------- | | New tab did not open | Allow pop-ups from the dashboard and retry | | Return to Openstead page | Sign in if needed and reopen the manager from Connections | | Manager not available | Check database running state, restore completion, role, and billing access | | Import too large or timed out | Use a smaller controlled import or a private migration client | | Two managers already open | Close unused sessions and wait for idle cleanup before preparing another | MySQL remains on the private network throughout. The HTTPS manager is not a public database port. # PostgreSQL (/databases/postgresql) Openstead PostgreSQL provides a private relational database with persistent storage and logical backups. Create the database in the same environment as the applications that use it. ## Create a PostgreSQL database [#create-a-postgresql-database] 1. Select your project and environment in the dashboard. 2. Choose **New → PostgreSQL**. 3. Enter the service name and choose the PostgreSQL version. Openstead supports major versions 16, 17, and 18; check application and extension compatibility before choosing. 4. Set the database name and username, or use the suggested values. 5. Choose a PostgreSQL plan and review its included storage. 6. Complete checkout for the service month and wait for provisioning to finish. PostgreSQL uses database-specific plans. See [Pricing](/billing/pricing) for the current plans. It does not have a Free database tier. ## Connect your application [#connect-your-application] Open **Connections** to find the private address. Authorised members can reveal the database name, username, password, port, and connection URL. PostgreSQL listens on port `5432`. Prefer a connection reference over copying a password. On the application, connect the database's **Connection URL** field to `DATABASE_URL`, then redeploy the application. Openstead resolves the value into the release environment. For an application using Node's `pg` library: ```ts import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 5, }); const result = await pool.query('SELECT current_database(), version()'); ``` Install the client library in your application's dependencies. Choose pool sizes for the database and the number of application replicas; every worker or replica can create its own pool. The hostname is reachable from applications in the same Openstead environment. Public desktop-client connections are not available. Do not add a public DNS record or open database ports as a workaround. See [Database connections](/databases/connections). ## Run migrations [#run-migrations] Run schema migrations from your application, using its configured database reference. On a paid application instance, a [pre-deploy command](/deployments/builds) or an explicit [shell or one-off command](/deployments/shell) can run the framework's migration command. Run a migration once per release, not independently in every web worker. Back up first and design changes to remain compatible with the previous application release when possible. ## Persistent storage [#persistent-storage] Openstead places the PostgreSQL cluster on its managed persistent volume. A normal container replacement preserves the database. Changing an application's image or redeploying it does not migrate or reset this database. Treat the database name, user, and major PostgreSQL version as initialisation choices. Major-version changes require a separate database and a deliberate migration. Do not attempt a major upgrade by replacing the image tag on an existing cluster. Paid plan upgrades can grow the included disk allocation. Downgrading does not shrink stored data; review the quoted retained storage when selecting a smaller plan. ## Backups and recovery [#backups-and-recovery] Daily backups are enabled by default. Configure retention in **Recovery**, from 1 to 30 days. A manual export creates a PostgreSQL custom-format `.dump` archive. Wait for a completed export before relying on it as a recovery point. Restoring a backup creates a separate database with its own capacity and credentials. Test the restored data, then change application references and redeploy. The source database is not overwritten. See [Backups and recovery](/databases/backups). Logical backups are discrete recovery points. They do not provide continuous point-in-time recovery, automatic failover, or backups of application uploads. ## Troubleshooting [#troubleshooting] | Symptom | Check | | ------------------------------ | ------------------------------------------------------------------------------------------------------ | | Hostname cannot be resolved | The connecting process is hosted in the database's environment, not on a laptop or another environment | | Password authentication failed | The application uses the current connection reference and has been redeployed | | Database or relation missing | Correct database name, migration history, and restore completion | | Too many connections | Pool size, replica count, background workers, and connections not being released | | Connection refused | Database running state, provisioning completion, and paid-through status | If storage verification or restoration reports a problem, retain the source database and its backups and contact [support](https://openstead.tech/contact) before deleting or recreating resources. # Builds and commands (/deployments/builds) Openstead builds repository code in an isolated build environment, then deploys the resulting files or container artifact. Your laptop's installed packages and local uncommitted files are not part of that build. ## Use the correct command for each phase [#use-the-correct-command-for-each-phase] | Setting | Purpose | Examples | | ------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Build command | Compile assets or application output after the builder prepares the project | `npm run build`, `python manage.py collectstatic --noinput` | | Pre-deploy command | Run a one-time release task before starting the new application release | `python manage.py migrate --noinput`, `php artisan migrate --force` | | Start command | Run the application process, worker, or scheduled command | `npm run start`, `gunicorn config.wsgi:application --bind 0.0.0.0:$PORT` | Leave a Railpack command empty to use its detected behavior. An explicit build command replaces the detected build command; make sure it performs every required application build step. A Dockerfile defines its own build instructions, so use its `RUN` steps rather than expecting the Railpack build-command field to rewrite the Dockerfile. ## Keep builds reproducible [#keep-builds-reproducible] * Commit dependency lockfiles such as `package-lock.json`, `pnpm-lock.yaml`, `poetry.lock`, `uv.lock`, `Gemfile.lock`, or `composer.lock`, as appropriate for your toolchain. * Declare the language version in your project's supported version file or manifest. * Avoid relying on packages installed globally on your development machine. * Keep secret `.env` files, local databases, and generated dependency folders out of Git. * Test the production build locally before deploying. The build environment should not depend on reaching a private runtime database. Run migrations in the pre-deploy phase, which can access the service's permitted private network. ## Run a pre-deploy command [#run-a-pre-deploy-command] Pre-deploy commands require a paid instance. They run with the new deployment's image and secret environment before the new release is activated. A nonzero exit code fails the deployment and preserves the previous release. The pre-deploy phase runs in a separate execution container. It does not mount the application's persistent disk, and its filesystem changes do not become part of the application image. Build assets during the build phase, not during pre-deploy. Pre-deploy execution is limited to 30 minutes, or a shorter configured execution timeout. Keep schema migrations backward compatible while the previous release is serving traffic. Pre-deploy commands can run again during a rollback, so they must also be safe to repeat. ## Caching and artifact reuse [#caching-and-artifact-reuse] Openstead maintains a private build cache for each service. Changes to the source, build settings, secrets, or builder can affect reuse. If the same service, commit, and build inputs already produced a usable immutable artifact, Openstead can reuse that artifact without running a new build. Pre-deploy tasks still run when configured. The public API's `clear-cache` service action requests a fresh build. Use it to investigate a cache-related failure, not as a routine requirement for every deployment. A normal redeployment can reuse a valid artifact. ## Build minutes [#build-minutes] Building and pre-deploy execution consume build minutes. Cloning, preparing infrastructure, pulling a release, and runtime health checks do not count as build execution. Workspace **Billing** shows included and purchased allowance. When build minutes run out, queued builds wait and active build execution can stop. The previous healthy release stays online. Purchase additional minutes or wait for the monthly allowance to reset; retry a build that already stopped. See [Billing](/billing/overview). ## Diagnose a build failure [#diagnose-a-build-failure] Read the first meaningful error in the build log. Later messages often just report the nonzero exit code. Check the application root, lockfile, language version, required variables, and whether a dependency needs system libraries. Use a [Dockerfile](/deployments/docker) for custom operating-system packages or a toolchain the automatic builder cannot prepare. # Dockerfiles and container images (/deployments/docker) Use Docker when you need precise control over system packages, language versions, runtime files, or a nonstandard application layout. You can build a Dockerfile from GitHub or deploy an existing container image. ## Build a Dockerfile [#build-a-dockerfile] 1. Commit a Dockerfile and `.dockerignore` to the repository. 2. Choose **Dockerfile** as the service's build method. 3. Set the application root, Dockerfile path, and Docker context. 4. Configure the service's listening port and runtime variables. 5. Deploy and inspect the build log. Both **Dockerfile path** and **Docker context** are relative to the selected application root. They cannot escape it with `..`. If a monorepo build needs shared files elsewhere in the repository, use the repository root as the application root and point to the nested Dockerfile. ## Example Node.js image [#example-nodejs-image] This example assumes an npm lockfile, production dependencies, and a `server.js` entry point. Applications that compile TypeScript or frontend assets should add a separate build stage. ```dockerfile FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --chown=node:node . . ENV NODE_ENV=production USER node EXPOSE 3000 CMD ["node", "server.js"] ``` Set Openstead's service port to `3000`, and have `server.js` listen on `0.0.0.0` using `process.env.PORT`. `EXPOSE` documents a port; it does not make the server bind to it. Example `.dockerignore`: ```text .git node_modules .env .env.* npm-debug.log ``` Do not ignore required checked-in configuration or generated assets your Dockerfile expects to copy. ## Build-time secrets [#build-time-secrets] Openstead passes configured environment variables to Docker builds as BuildKit secrets, not as automatic Docker `ARG` values. Reference a secret explicitly in the Dockerfile: ```dockerfile # syntax=docker/dockerfile:1 RUN --mount=type=secret,id=PRIVATE_TOKEN,required=true \ PRIVATE_TOKEN="$(cat /run/secrets/PRIVATE_TOKEN)" ./scripts/install-private-dependencies.sh ``` Add `PRIVATE_TOKEN` to the service's environment. The script must avoid printing the token or saving it into the final filesystem. A secret mount protects how a value is supplied; your build can still leak it if you copy it into output. ## Deploy an existing image [#deploy-an-existing-image] Choose **Container image** as the source and provide a registry reference such as `ghcr.io/your-team/your-app:release`. For private images, select an authorized registry connection. Private registry access requires the applicable paid workspace features. Openstead resolves the image to an immutable digest for the release. Publishing a new image under the same tag does not replace a running release automatically; deploy the image again. Use digest references when you want to identify an exact artifact yourself. Keep the service start command empty to retain the image's entry point and command unless an override is required. Ensure the image can run as a foreground service and includes any shell needed for command-based operations. ## Persistent data and migrations [#persistent-data-and-migrations] Image files are replaced by subsequent releases. Store application data in [managed databases](/databases/postgresql), a persistent disk, or object storage. Run one-time migrations in a paid [pre-deploy command](/deployments/builds), not in a Dockerfile build step that needs the private runtime database. # Environment variables and secrets (/deployments/environment-variables) Use environment variables for configuration that changes between deployments or contains secrets. Common examples include database URLs, application keys, API credentials, and public frontend configuration. ## Add a service variable [#add-a-service-variable] 1. Open the service's **Environment** page. 2. Select **Add variable** and enter its key and value. 3. Save the variable, then deploy the service. Read the value using your framework's environment API: ```js const databaseUrl = process.env.DATABASE_URL; ``` ```python import os database_url = os.environ["DATABASE_URL"] ``` Use a required lookup for a secret your app cannot operate without. Silently falling back to a development database or default password can make a deployment appear healthy while using the wrong data. ## Share configuration with environment groups [#share-configuration-with-environment-groups] Create an **Environment Group** from the workspace navigation and add the variables or secret files that related services should share. Link the group from each service's Environment page. Groups can be scoped to an environment, so use separate groups for production and test credentials. Service-level values override matching group values. Avoid defining the same key in several linked groups; if an override is intentional, put it directly on the service so its precedence is clear. Linked-service references cannot use a key already defined by a variable; remove the duplicate before deploying. Group edits apply to future deployments. Deploy each linked service that must receive an updated value. Groups linked to protected services require owner or admin access. A service that inherits protected configuration keeps that restriction even after unlinking the group, because its current or retained deployments can contain the copied values. The same rule covers protected connection references and preview copies. See [environment protection](/account/projects#protect-an-environment). ## Mount a secret file [#mount-a-secret-file] Under **Secret Files**, add a filename and its contents. At runtime, the file is mounted read-only at: ```text /etc/secrets/ ``` For example, a file named `credentials.json` is available at `/etc/secrets/credentials.json`. Configure your application to read that path. Do not write back to the mounted file or assume it exists on your laptop. Secret files are runtime mounts. For a Docker build that needs a variable as a secret, use the supported [BuildKit secret mount](/deployments/docker#build-time-secrets) rather than copying private files into the image. ## Build-time and runtime values [#build-time-and-runtime-values] Railpack receives configured variables during the build and the application receives its deployment's variables at runtime. Variables compiled into a frontend become public: `NEXT_PUBLIC_*`, `VITE_*`, and similar mechanisms expose their values in files delivered to the browser. Never put a database password, private API key, or Openstead API token into a browser-exposed variable. Keep it in a backend service and expose only the necessary application endpoint. Openstead sets runtime `PORT` from the service's port setting. Configure that setting to match your server rather than using a competing `PORT` value in a shared group. ## Rotate secrets carefully [#rotate-secrets-carefully] Add the new credential, deploy, and verify the application before revoking the old one at its provider. Some rotations require a period in which both credentials remain valid. Deployment records retain their captured environment for release recovery. A [rollback](/deployments/rollbacks) can restore older secret values. If a compromised credential has been revoked, deploy compatible code with the current environment rather than blindly restoring the old snapshot. Avoid logging secrets. Redaction helps with recognized values, but it cannot make arbitrary transformed or application-generated sensitive output safe to publish. # Connect GitHub (/deployments/github) Openstead uses a GitHub App connection to discover repositories, read the selected revision, and receive deployment events. Connecting GitHub to a workspace is separate from choosing GitHub as your account's sign-in method. ## Connect a repository [#connect-a-repository] 1. Start creating a service and choose a Git repository as its source. 2. Connect the GitHub account or organization that owns the repository. 3. In GitHub's installation screen, grant the Openstead app access to the repositories you want to deploy. 4. Return to Openstead, refresh the repository list if necessary, and select a repository and branch. 5. Select the application root when the repository contains more than one application. Detection reads the repository's manifests and suggests commands. Review those suggestions before deploying, particularly in monorepos and applications with custom entry points. ## Change repository access [#change-repository-access] Use **Configure repository access** beside the GitHub connection to change the installation's repository selection. For organization-owned repositories, you may need an organization owner to approve the installation or repository access. If GitHub shows 404 on an installation settings page: * Confirm you are signed into the GitHub account that can manage that installation. * Check that the app is still installed and has not been suspended or removed. * Open the owning account or organization's GitHub App installation settings to locate the current installation. * Reconnect in Openstead if the original installation was removed and recreated. Do not assume an old installation URL remains valid after reinstalling the app. ## Choose an automatic deployment policy [#choose-an-automatic-deployment-policy] In the service's settings, set **Auto-deploy**: | Policy | Behavior | | -------------------- | ------------------------------------------------------------------------------------------------- | | Off | Release manually or through your own automation | | On every commit | A push to the configured branch can trigger a deployment | | After CI checks pass | Release the current branch head after GitHub checks and commit statuses report acceptable results | For the CI policy, a failed or incomplete check prevents the release. Confirm that your workflow actually runs for the configured branch and publishes its results to the same commit. Configure [build paths](/deployments/monorepos) to narrow push-triggered deployments to relevant repository changes. ## Private repositories and trusted code [#private-repositories-and-trusted-code] Only repositories visible to the connected installation are available. Keep the installed app's access as narrow as your project allows. Contributors who can change deployed code can execute that code during the build or runtime, so review repository permissions alongside workspace permissions. Pull-request previews have additional trust rules. In particular, automatic previews do not receive service secrets from forked repositories. See [Preview environments](/deployments/previews). # Health checks (/deployments/health-checks) A process can be running before it is ready to serve traffic. A health check helps Openstead distinguish a started container from an application that can accept requests during deployment, scaling, and ongoing operation. ## Understand service status [#understand-service-status] Your service's current status is separate from the outcome of a previous deployment. **Deploy succeeded** records that a release completed successfully. **Live** requires the active release to have a recent successful readiness observation. | Status | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------ | | Live | The current release has a recent successful readiness check. | | Unavailable | The runtime is missing or no expected instance passed its readiness check. | | Degraded | Some expected instances are ready, but the requested instance count is not healthy. | | Status unknown | Openstead could not obtain a current observation, or the previous observation is more than five minutes old. | | Deleting | Removal was requested and infrastructure cleanup is still in progress. | | Deleted | Openstead confirmed runtime and routing cleanup. The service's controls are no longer available. | The console refreshes service state across open views. A network failure displays unknown status instead of continuing to claim the service is live. Deletion continues in the background and retries incomplete cleanup; a service disappearing from the normal list is not, by itself, confirmation that removal has finished. ## Configure an HTTP check [#configure-an-http-check] Add an application route such as `/health`. For a web service, set **Health check path** to that path in the service settings. For a private service, set `configuration.healthCheckPath` through [Update service](/api/reference/services/updateService). Deploy to apply the new release configuration. Example Express route: ```js app.get("/health", (_request, response) => { response.status(200).json({ status: "ok" }); }); ``` The path must begin with `/`. The current readiness probe issues a `GET` to the application's configured port and accepts HTTP `200` through `399`. Aim for a direct `200` response rather than a redirect. The probe does not follow redirects and uses a short connection/read timeout. ## Keep the route independent of a browser [#keep-the-route-independent-of-a-browser] The endpoint must work without authentication, cookies, a CSRF token, or a custom browser header. The readiness probe reaches the instance directly rather than through its custom domain, so hostname-dependent middleware can reject it even when the public homepage works. For frameworks with allowed-host enforcement, explicitly handle the readiness path safely. Do not disable host validation for the entire application just to make a health probe pass. Keep the health response minimal and avoid exposing configuration or database details. ## Check meaningful readiness [#check-meaningful-readiness] A basic route proves that the application server can respond. If the app cannot serve useful requests until a critical dependency is available, add a fast, bounded readiness check for that dependency and return `503` when it is unavailable. Avoid expensive queries, schema migrations, payment-provider calls, and other side effects in the health endpoint. A fragile optional third-party API should not make every application instance fail readiness. ## Without a path [#without-a-path] When no HTTP path is configured, Openstead checks whether the configured application port accepts a TCP connection. This is useful for initial bring-up, but it cannot confirm that the application returns useful HTTP responses. Workers do not expose an HTTP service for this check. Verify queue connectivity and job completion using their logs and application monitoring. ## Diagnose a failed readiness check [#diagnose-a-failed-readiness-check] 1. Confirm the application listens on `0.0.0.0` and the service's configured `PORT`. 2. Inspect runtime logs for startup exceptions or missing variables. 3. Confirm that the health path exists and does not require login. 4. Check allowed-host, forced-HTTPS, and other middleware behavior for direct instance requests. 5. Check memory usage and initialization time if the application restarts during startup. A public URL returning an error does not always mean the health route failed, and a passing health route does not validate every user flow. Test the application after deployment as well. # Logs and metrics (/deployments/logs) Openstead separates build output, application runtime output, and platform messages. Start with the deployment log when a release fails; start with runtime logs when a live application returns an error. ## Read service logs [#read-service-logs] Open **Logs** in the service navigation. Select a time range and use the search field to narrow the output. You can also open a deployment to see output associated with that release. Applications should write operational logs to standard output and standard error. A framework that writes only to a file inside the container will not automatically expose that file in the service log viewer. For Python, enable unbuffered output when needed: ```text PYTHONUNBUFFERED=1 ``` For Laravel, use a logging configuration that sends relevant output to standard error rather than only `storage/logs`. ## Search and download [#search-and-download] Search for an exception name, request identifier, queue job ID, or another stable field. Select the time range around the failure. The download control exports the currently displayed lines; it is not a promise of an unlimited full-history export. Runtime output is collected periodically, so a new log line can take a short time to appear. Missing output can also mean the process never started, the time filter excludes it, or the application is writing to a file instead of stdout/stderr. ## Retention [#retention] | Active instance plan | Available log history | | -------------------- | --------------------- | | Free or Starter | 7 days | | Builder | 14 days | | Growth or Scale | 30 days | Access to longer history follows the active paid instance, not an unsaved or undeployed plan selection. Do not rely on the dashboard as the only archive for records that must be retained longer. ## Inspect metrics [#inspect-metrics] The **Metrics** page shows observed CPU utilization, memory use, network activity, and instance counts for running containers. Metric history is retained for up to seven days. Use it to correlate a failure with a deployment or change in resource use. * High CPU with a growing queue can indicate that the worker needs more capacity or less expensive work per job. * Memory near the limit can indicate an undersized instance, excessive concurrency, or a memory leak. * An unexpected instance count can indicate a restart or a scaling operation. Network totals and resource samples are not application tracing or request-latency percentiles. Add application instrumentation when you need route-level timings or distributed traces. Static releases have no dedicated application container, so they do not provide container CPU or memory metrics or application runtime logs. Their build logs remain available. ## Keep logs useful and safe [#keep-logs-useful-and-safe] Include timestamps, severity, and correlation IDs. Avoid logging passwords, tokens, full payment data, or unnecessary personal information. Openstead redacts recognized deployment secrets, but encoded or transformed values can escape simple redaction. For recovery steps, see [Troubleshooting](/guides/troubleshooting). For resource changes, see [Scaling](/deployments/scaling). # Monorepos (/deployments/monorepos) Each Openstead service can point to a different application in the same GitHub repository. The repository is shared; the build configuration, variables, plan, and release history belong to each service. ## Independent application directories [#independent-application-directories] Consider this repository: ```text apps/ web/package.json api/pyproject.toml packages/ shared/ ``` If `apps/web` and `apps/api` are independently installable, create two services and set their **Root directory** to `apps/web` and `apps/api`. Commands run relative to the selected application root. Do not include that path again in commands or publish-directory settings unless the tool expects it. ## Shared package workspaces [#shared-package-workspaces] For a pnpm, Yarn, npm, or other workspace that needs the repository's top-level lockfile and shared packages, use the repository root as the service root. Set a build command targeting the intended package, such as: ```bash pnpm --filter @example/web build ``` The package name must match your repository. Set the corresponding start command for a web service or the generated publish directory for a static site. Ensure dependent packages are built too; this is controlled by your workspace build tool. A Dockerfile is useful when the selected app needs files from several directories. Keep the application root at the repository root, use a nested Dockerfile path such as `apps/api/Dockerfile`, and set Docker context to `.`. ## Filter automatic deployments [#filter-automatic-deployments] Service settings include **Included build paths** and **Ignored build paths**, with one pattern per line. Push-triggered deployment filtering works on repository-relative changed paths, including the application root prefix. For a frontend built from the repository root, include relevant patterns such as: ```text apps/web/* packages/shared/* package.json pnpm-lock.yaml pnpm-workspace.yaml ``` These are wildcard path patterns, not regular expressions. Ignored matches take precedence over included matches. When a nonempty root is selected, changes outside that root do not pass its root filter. Use the repository root if shared packages outside an app directory must trigger its deployments. Path filtering applies to push-triggered automatic deployments. Do not rely on those filters to isolate the **After CI checks pass** policy; configure the CI workflow and service policy deliberately. If a webhook does not include enough changed-file information, Openstead can deploy rather than incorrectly skip a change. ## Coordinate related releases [#coordinate-related-releases] Changing a shared interface can require more than one service deployment. Keep API changes backward compatible while releases roll out. Use a [Blueprint](/blueprints/overview) to describe related configuration, and inspect each service's result rather than treating the repository as one atomic application release. Common failures include a missing root lockfile, a shared package excluded from the build context, and a publish directory that repeats the application root. Check those paths first when the same build works locally. # Deployments (/deployments/overview) A deployment records the source, configuration, and environment used to release a service. Saving service settings and deploying those settings are separate actions. For application code, commands, variables, and disk mounts, deploy after saving to apply the new configuration. ## Start a deployment [#start-a-deployment] Open the service and choose **Manual Deploy → Deploy latest commit**. To release a particular repository revision, choose **Deploy a specific commit** and provide its full commit SHA. Image-based services offer **Deploy latest image**. Cron jobs call these actions **Manual Build** because a successful build prepares the image for later scheduled runs. You can also configure [automatic deployments](/deployments/github), use the public API, or deploy from the Openstead CLI. ## Follow the release lifecycle [#follow-the-release-lifecycle] | Status | What is happening | | ---------- | ---------------------------------------------------------------- | | Queued | The request is waiting for available build capacity or allowance | | Preparing | Openstead is preparing the build and runtime resources | | Cloning | The selected repository revision is being fetched | | Building | Dependencies and application output are being built | | Predeploy | The optional pre-deploy command is running | | Deploying | The artifact is being released to the runtime | | Checking | Openstead is checking that the new release is ready | | Live | The release passed its required checks | | Failed | The release could not complete; inspect the error and logs | | Cancelled | Cancellation stopped the deployment | | Superseded | A newer queued deployment replaced this request | An unchanged artifact can be reused, so not every release performs every build phase. Static sites publish files; container services start application instances. ## Understand configuration snapshots [#understand-configuration-snapshots] Each deployment captures the relevant configuration and secrets when it is queued. Editing a variable while a build is running does not alter that deployment's captured value. Save the correction and start another deployment. The **Live** deployment identifies what is serving. A newer failed deployment can appear above it in history while the previous release continues to serve. Inspect both deployment history and runtime status when diagnosing availability. ## Concurrent changes [#concurrent-changes] Openstead serializes deployments for a service. Workspace settings control whether a new deployment cancels the currently running deployment or waits for it. In both cases, a newer waiting deployment supersedes older waiting deployments so obsolete commits do not accumulate in the queue. Build concurrency and included build minutes belong to the workspace. A queued message explains when builds are waiting for capacity or allowance. Repeatedly clicking deploy does not increase that capacity. ## Release safely [#release-safely] Use [health checks](/deployments/health-checks), keep database migrations compatible with the previous release, and verify an important user flow after the release becomes live. Failed builds and pre-deploy commands preserve the previous release. Runtime recovery can require attention if the new release or its restoration cannot become healthy; a rolling deployment is not an unconditional zero-downtime guarantee. Use [rollbacks](/deployments/rollbacks) to restore a previously successful artifact. Database contents and user uploads require their own recovery strategy. # Preview environments (/deployments/previews) Preview environments create a separate service for a pull request from the connected repository. Use them to review a frontend or application change without replacing the parent service's live release. ## Enable previews [#enable-previews] In service settings, choose a **Preview environments** policy: | Policy | Behavior | | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | | Off | Pull requests do not create previews | | Create manually | A trusted pull request needs the `openstead-preview` label or `[openstead preview]` in its title to start a preview | | Create for pull requests | Eligible pull requests create previews automatically | Previews apply to web services, static sites, private services, and background workers. Use the service's **Previews** page to inspect its preview services, URLs where applicable, and expiry times. ## Repository trust and secrets [#repository-trust-and-secrets] Previews run pull-request code. Openstead skips pull requests from forks so untrusted fork code does not automatically receive the parent service's secrets. Eligible same-repository previews copy the parent's service variables and linked environment-group values when the child service is created. Treat collaborators who can push a preview branch as trusted with those values. Use test credentials for services that will create previews. Preview service configuration is a copy, not a live link to every later parent setting. Inspect the preview's own environment before testing changes that depend on newly added variables. ## Databases and persistent data [#databases-and-persistent-data] A preview receives an isolated environment. It does not automatically clone your production database, local upload disk, or related service graph. A private production database URL copied as a plain variable may not be reachable from the preview's isolated network, and an externally reachable production credential can still be dangerous. Provision test dependencies in the preview's permitted network scope and use separate test data. Avoid payment, email, and other irreversible production side effects in preview code. ## Plans and expiry [#plans-and-expiry] Preview compute is a separate service. A paid parent service's active month does not pay for the preview. A preview that requires paid compute waits for its own payment before deploying; creating a pull request does not authorize a purchase. Set **Expiry after inactivity** between 1 and 720 hours. Relevant pull-request activity refreshes the preview's expiry. Closing the pull request expires its preview and schedules cleanup. Do not keep irreplaceable data only in a preview. To skip preview creation, add the `openstead-preview-skip` label or `[skip preview]` to the pull request's title. Adding a skip marker is not a substitute for explicitly managing an already running preview. # Rollbacks and recovery (/deployments/rollbacks) A rollback creates a new deployment using a previous successful release of the same service. It reuses that release's retained artifact and deployment snapshot instead of rebuilding the current branch. ## Roll back a service [#roll-back-a-service] 1. Open the service's deployment history. 2. Select a successful release that offers **Rollback**. 3. Review the source revision and confirm the service name when prompted. 4. Follow the new deployment through readiness checks. 5. Verify the public endpoint and a representative application operation. Only successful releases with a retained image or static artifact are eligible. A failed build is not a rollback target. Rollback can still fail if the artifact is unavailable, the selected configuration cannot be admitted, or the application is no longer compatible with its dependencies. ## What a rollback restores [#what-a-rollback-restores] The release captures the source revision, artifact, configuration, and secret snapshot. Restoring it can therefore restore old environment values, commands, and instance settings. Review paid-compute authorization if its resource configuration differs from the current one. The rollback does not rewrite the current repository branch. A subsequent deployment of the latest commit releases that branch using the service's saved configuration. Update your repository and saved settings deliberately so the next automatic deployment does not reintroduce the problem. ## What a rollback does not reverse [#what-a-rollback-does-not-reverse] * Database migrations, inserts, updates, or deleted records. * Files changed on a persistent disk or in object storage. * Emails, payments, webhooks, and other external side effects. * Credentials you revoked at an external provider. Use [database backups](/databases/backups) for data recovery, and keep disk or object-storage backups separately. A code rollback should be compatible with the database schema still in use. ## Plan backward-compatible migrations [#plan-backward-compatible-migrations] Use additive changes first: introduce a new column, deploy code that can read both formats, migrate existing data, and remove old fields only after old releases are no longer needed. Pre-deploy commands can run again during a rollback. They must be safe to repeat and must not assume that every release moves forward to a new schema. ## Recovery needs attention [#recovery-needs-attention] If Openstead cannot verify a healthy release after a release failure and recovery attempt, the service can enter **Needs attention**. Avoid repeatedly queuing deployments against that state. Contact [support](https://openstead.tech/contact) with the service ID, deployment ID, timestamp, and error text so the retained release can be checked safely. # Languages and detection (/deployments/runtimes) Openstead inspects the selected application's files and uses Railpack to prepare supported runtimes. Detection gives you a starting configuration; it cannot know every application's entry point, migration policy, or production secrets. ## What detection reads [#what-detection-reads] | Project files | Detection | | ---------------------------------------------------------------- | --------------------------------------------------- | | `package.json` | Node.js and common JavaScript frameworks | | `requirements.txt`, `pyproject.toml`, or `Pipfile` | Python, with Django, FastAPI, and Flask suggestions | | `composer.json` with `artisan` or Laravel's framework dependency | Laravel / PHP | | `composer.json` | PHP | | `go.mod` | Go | | `Gemfile` | Ruby | | `Cargo.toml` | Rust | | `pom.xml` | Java / Maven | | `build.gradle` or `build.gradle.kts` | Java / Gradle | | `.csproj` or `.fsproj` | .NET | | `mix.exs` | Elixir | | `deno.json` | Deno | | `index.html` without a recognized application manifest | HTML static site | | `Dockerfile` | Dockerfile build takes precedence | Framework recognition and automatic build-provider support are different. If the selected builder cannot build your project's language or version, provide a Dockerfile with the exact toolchain instead of assuming a detected label guarantees a working build. ## JavaScript frameworks [#javascript-frameworks] Openstead identifies Next.js, Nuxt, Astro, SvelteKit, Vite, Create React App, Express, and NestJS from package metadata. A Next.js project with static export is suggested as a static site; a server-rendered Next.js application is a web service. Astro's server output needs an appropriate adapter. SvelteKit needs the adapter for your intended deployment mode. Package-manager suggestions follow committed lockfiles: pnpm, Yarn, Bun, then npm. Keep a single intended package-manager lockfile and review the generated command. ## Python applications [#python-applications] Django detection can suggest Gunicorn when it finds a `wsgi.py` module. FastAPI and Flask use conventional sample entry points, which you must adjust if your module or application object has a different name. Ensure the chosen server is included in your production dependencies. ## Laravel applications [#laravel-applications] A Laravel application's `package.json` usually builds browser assets. Its presence does not make the application a static Vite site. Openstead prioritizes Laravel recognition and leaves commands available for the PHP provider's combined Composer and asset handling. Set the correct root containing `composer.json` and `artisan`. Packaged scripts with nonstandard public directories may need a Dockerfile. See [Deploy Laravel](/guides/laravel). ## Pin the runtime [#pin-the-runtime] Use version files and manifest fields supported by your build toolchain. When you require an exact OS image, PHP extension set, or custom native library, pin it in a [Dockerfile](/deployments/docker). Review the version reported in the build log after an upgrade rather than assuming it matches your laptop. For repositories with multiple applications, select each root deliberately. See [Monorepos](/deployments/monorepos). # Scaling and compute (/deployments/scaling) Openstead offers instance plans and replica settings for application workloads. Increasing an instance's resources and adding more instances solve different problems: a memory-heavy single task may need a larger instance, while independent HTTP requests or queue jobs may benefit from more instances. ## Change instance size [#change-instance-size] Open the service's **Compute** page and review the available plan, quote, and effective date. Complete any required payment before deployment. A change purchased during an existing paid month takes effect at that month's end; it does not immediately replace the current paid configuration. See [Renewals and plan changes](/billing/renewals) before scheduling a capacity change. When the new configuration is active and eligible for deployment, deploy it and compare memory and CPU usage with the previous release. Plan selection in a form does not activate paid resources by itself. Use the service's billing and deployment status to confirm what is actually running. ## Manual replicas [#manual-replicas] Paid web services, private services, and background workers can use multiple instances where capacity permits. Set the replica count, save, and deploy. The configuration accepts counts from 1 to 20, but the workspace must have capacity and billing authorization for the requested resources. Free instances cannot be scaled. Services with a local persistent disk require one instance. Managed databases have their own compute and storage rules; this replica setting is not database replication. ## Automatic scaling [#automatic-scaling] For an eligible paid service without a persistent disk: 1. Set **Scaling mode** to **Automatic**. 2. Choose minimum and maximum instances. 3. Set target CPU and memory utilization. 4. Review the resource quote, save the policy, and deploy. Autoscaling uses sustained observed CPU or memory utilization, not a single spike. It increases or decreases the desired count gradually and uses cooldowns to reduce oscillation. Missing or stale samples do not trigger a scale-down. Scaling stays within the configured bounds and available workspace capacity. It does not provision unlimited underlying capacity on demand. If capacity prevents the requested change, the service reports the issue; contact [support](https://openstead.tech/contact) or reduce the request. ## Make the application safe for replicas [#make-the-application-safe-for-replicas] * Store sessions in a shared store or signed cookies rather than instance memory. * Store uploads in object storage instead of a local instance filesystem. * Use database connection-pool limits that account for every instance. * Keep scheduled tasks in a separate cron job so each web replica does not run the same schedule. * Make queue jobs safe to repeat and confirm multiple consumers are supported. For server-rendered frameworks, also review cache consistency and release-specific state. Adding replicas does not automatically make an application designed for one process distributed. ## Inspect the result [#inspect-the-result] The scaling panel distinguishes confirmed instances from desired instances. Wait for the operation to converge and check application health. A saved policy is applied on the next deployment; it is not an immediate change to the current release. Review [Logs and metrics](/deployments/logs) before scaling further. More resources can mask a leak or a slow query without fixing it. # Shell access and one-off jobs (/deployments/shell) Openstead provides two ways to execute application commands: an interactive shell inside a running instance and a one-off command in a fresh execution container. Both require an eligible active paid application service and permission to operate it. ## Open an interactive shell [#open-an-interactive-shell] 1. Open the service's **Shell** page. 2. Select an available instance if the service has several. 3. Connect and run the diagnostic command. 4. Disconnect when finished. The session is attached to the selected live instance. It can inspect files and mounted data visible to that instance. Commands can change production state, so treat the shell with the same care as application administration. Sessions expire after 20 minutes. An account can have two active terminal sessions at a time. If a deployment replaces the instance, refresh the instance list and reconnect. ## Run a one-off command [#run-a-one-off-command] Use **Run a command** or **One-Off Jobs** for a command that should execute with the current release image and environment and retain its output and exit status. Examples include: ```bash python manage.py check --deploy ``` ```bash php artisan migrate:status ``` Each one-off command runs in a fresh container with access to the permitted private network. It does not share the running application's writable filesystem or persistent disk. Files created in that execution are not a way to update the deployed application's source or upload directory. Use a deployment's [pre-deploy command](/deployments/builds) for migrations that must gate a release. Use a [cron job](/services/cron-jobs) for a repeating schedule. ## Availability by service [#availability-by-service] | Service | Interactive shell | One-off application commands | | ---------------------------------------------------- | -------------------------------------- | ----------------------------------------------- | | Paid running web service, private service, or worker | Yes, when a live instance is available | Yes | | Static release | No application container | No application container | | Cron job | No permanent instance | Use its run controls and history | | Managed database | No application shell | Use its database connection or management tools | | Free web instance | Requires an upgrade | Requires an upgrade | For MySQL administration, see [MySQL](/databases/mysql). Do not use application shell credentials as a replacement for database access controls. ## Make lasting changes through deployments [#make-lasting-changes-through-deployments] Editing source code in a shell is temporary and bypasses your repository history. Commit the change to Git and deploy it. Store durable application data in its database, persistent disk, or object storage, and take an appropriate backup before changing production records. # Frequently asked questions (/getting-started/faq) ## Do I need to bring my own server? [#do-i-need-to-bring-my-own-server] No. Openstead supplies and manages the infrastructure used by your services. You connect source code or a container image and configure the application. ## Can I deploy a private GitHub repository? [#can-i-deploy-a-private-github-repository] Yes. Connect the Openstead GitHub App and grant it access to the repository. GitHub account sign-in alone does not grant repository deployment access. Organisation installations may require an organisation owner. See [GitHub](/deployments/github). ## Does Openstead detect my framework? [#does-openstead-detect-my-framework] Openstead inspects supported application files when a repository is selected. It can propose runtime, directory, and build settings. Review the result for monorepos and unusual project layouts. A Dockerfile is available when you need explicit control of the build environment. ## Can I run Laravel with MySQL? [#can-i-run-laravel-with-mysql] Yes. Deploy Laravel as a web service, create MySQL in the same environment, and add database connection references. Put uploads on persistent storage and preserve the application's existing encryption key when migrating. See [Laravel](/guides/laravel). ## Can I connect to a database from my laptop? [#can-i-connect-to-a-database-from-my-laptop] Managed database ports are private to Openstead applications in the same environment. There is no public PostgreSQL, MySQL, or Key Value address to paste into a desktop database client. MySQL includes [phpMyAdmin](/databases/phpmyadmin), opened securely from the dashboard. ## Will application files survive a deployment? [#will-application-files-survive-a-deployment] Only files stored on the service's configured [persistent disk](/databases/persistent-disks) are intended to survive container replacement. Treat other writable container files as temporary. A managed database includes its own persistent data storage. Database backups do not include uploaded application files. ## Can I use my own domain? [#can-i-use-my-own-domain] Yes, on web services and static sites. Add the hostname, publish the supplied ownership TXT and routing record, and allow Openstead to verify DNS and activate HTTPS. Cloudflare records must use DNS-only mode for the current verification flow. See [Custom domains](/networking/custom-domains). ## Why does a free application take time to open? [#why-does-a-free-application-take-time-to-open] A Free web instance sleeps after inactivity and starts when a visitor arrives. Paid web instances do not use the free idle-sleep policy. Static sites and MySQL Free also do not use that policy. ## Do I have to save a card? [#do-i-have-to-save-a-card] No. Paid service months use hosted checkout. Renewal is manual, with an invoice issued two days before the current month ends. A three-day grace period follows expiry; unrenewed compute then pauses and database data and persistent disks are retained. See [Renewals](/billing/renewals). ## Does a successful payment mean my application is live? [#does-a-successful-payment-mean-my-application-is-live] No. Confirmed payment purchases capacity, then provisioning and deployment run separately. If a paid deployment fails, review its deployment error and use the available retry flow. Check the existing purchase before paying again. ## How do I get help? [#how-do-i-get-help] Check [System status](https://status.openstead.tech) for known platform incidents and maintenance. Review incident updates and history without signing in to the Openstead console. Read [Platform status and incidents](/guides/platform-status) for the difference between platform availability and an individual application's health. Contact [Openstead support](https://openstead.tech/contact). Include the service name or ID, the time of the problem with a timezone, the deployment or request ID, and the relevant error text. Describe what you expected and what happened. Remove passwords, API keys, database URLs, payment details, and customer data from logs or screenshots. # Deploy your first application (/getting-started/first-deploy) This guide takes an application already in GitHub and publishes it on Openstead. You need access to the repository and a verified Openstead account. ## 1. Create a project [#1-create-a-project] Open the [Openstead dashboard](https://openstead-dashboard.vercel.app/dashboard) and select your workspace. Create a project with a recognisable name. Openstead creates a Production environment for a new project. Projects organise related services; they are not billable server instances. See [Projects and environments](/account/projects) for a production and staging layout. ## 2. Choose the service type [#2-choose-the-service-type] Select **New** and choose **Web Service** if your application runs a server process. Choose **Static Site** if its build produces only HTML, CSS, JavaScript, and other static assets. A Next.js application using server rendering needs a web service. A Vite frontend that builds into `dist` normally needs a static site. A Laravel application needs a web service and a separate MySQL database. ## 3. Connect your repository [#3-connect-your-repository] Connect GitHub and authorise the Openstead GitHub App for the repository. Signing in with GitHub and granting deployment access are separate actions. You can grant selected repositories instead of all repositories. Select the repository and branch. For a monorepo, set **Root Directory** to the application directory, such as `apps/api`. Openstead checks the selected source and proposes supported runtime and build settings. If the repository does not appear, use **Configure repo access**, check the correct GitHub personal or organisation account, and grant the Openstead installation access. An organisation owner may need to approve the installation. See [GitHub deployments](/deployments/github). ## 4. Review the configuration [#4-review-the-configuration] | Setting | What to check | | ----------------------- | ----------------------------------------------------- | | Name | A unique, recognisable name for this service | | Project and environment | The intended application group and network boundary | | Root directory | The directory containing this application's manifest | | Build command | The command that installs or compiles the application | | Start command | The long-running production command for a web service | | Port | The port your application actually listens on | | Publish directory | The generated output directory for a static site | | Environment variables | Required configuration and secrets | | Instance type | Resources and price appropriate to the application | Web applications must listen on `0.0.0.0`, not only `localhost`. Use the configured port consistently. Run a production server, rather than a development watcher. Add credentials in [Environment variables](/deployments/environment-variables). Browser-exposed frontend variables must never contain database passwords or Openstead API keys. If the app needs a managed database, create it in this environment first and add a [connection reference](/databases/connections). Wait for the database to be running before deploying the application that depends on it. ## 5. Deploy [#5-deploy] Review the selected plan. Free services do not require checkout. For paid compute, review and confirm the service-month quote, complete hosted checkout, and return to Openstead. Payment confirmation and application deployment are separate stages. Start the deployment and follow the build logs. A successful build produces an application release; Openstead then starts the service and checks its health. The page shows whether the deployment is queued, building, deploying, live, or failed. ## 6. Verify the live application [#6-verify-the-live-application] Open the service's public URL after the deployment is live. Test more than the homepage: check authentication, a database-backed page, static assets, and an upload if your application supports them. Use **Logs** to inspect application output. A running container can still return an application error if a migration, secret, or framework setting is missing. See the matching framework guide before running migrations or changing production configuration. ## Next steps [#next-steps] * [Connect your own domain](/networking/custom-domains). * [Create a database backup](/databases/backups) before a schema change. * [Enable the deployment behaviour you want](/deployments/overview) for future Git pushes. * [Invite your team](/account/team-members) and [enable two-factor authentication](/account/security). If deployment fails, inspect the first meaningful error in the build or application logs and follow [Troubleshooting](/guides/troubleshooting). Repeatedly clicking Deploy without correcting the cause usually produces the same result. # Free instances (/getting-started/free-instances) Openstead offers free static hosting, Free web instances, and one MySQL Free database per workspace. These are separate products with different limits. ## Compare the free options [#compare-the-free-options] | | Static site | Free web service | MySQL Free | | --------------------------- | --------------------------- | --------------------------------- | -------------------------- | | Compute price | $0 | $0 | $0 | | Runs a server process | No | Yes | Managed MySQL | | Memory | Not an application instance | 512 MiB | 768 MiB | | CPU | Not an application instance | 0.1 shared vCPU | 0.25 shared vCPU | | Persistent database storage | None | None | 2 GiB total allocation | | Idle sleep | No web-process sleep | After 15 minutes of inactivity | No idle sleep | | Monthly runtime allowance | No free-web hours consumed | 750 hours shared by the workspace | No free-web hours consumed | ## Free web services [#free-web-services] Free web services are useful for experiments and applications that can tolerate a cold start. After 15 minutes without activity, the service sleeps. A visitor request starts it again; the browser may display a starting page while the process becomes ready. The workspace shares **750 running hours per UTC calendar month** across its Free web instances. Two continuously running services consume that allowance faster than one. When the allowance is exhausted, affected Free services wait for the next UTC month or an explicit paid upgrade. Openstead does not automatically buy an upgrade. Free web instances have no persistent application disk, paid shell, pre-deploy command, or multi-replica scaling. Files written to the container filesystem should be treated as temporary. Store application data in a database and use durable storage for uploads. ## Static sites [#static-sites] A static site serves built files and does not run your Node, PHP, or Python server. It does not consume the free web runtime-hour allowance. Builds still use the workspace's build-minute allowance. Use a web service if your site needs server-side rendering, server routes, or a process that handles requests dynamically. ## MySQL Free [#mysql-free] MySQL Free includes MySQL 8.4, private connections, up to 25 simultaneous connections, daily backups enabled by default, and up to seven days of backup retention. Its 2 GiB allocation includes MySQL system files and logs as well as your data. One free MySQL slot is available per workspace. Pending, suspended, and archived databases still occupy the slot. A completed deletion or a successfully running paid upgrade releases it. It has no automatic expiry and does not follow the Free web sleep policy. See [MySQL](/databases/mysql) for its limits and [phpMyAdmin](/databases/phpmyadmin) for browser-based management. ## Shared workspace limits [#shared-workspace-limits] A workspace without qualifying paid services includes one member, up to two environments per project, 25 configured services, at least 500 shared build minutes per UTC month, and one concurrent build. Creating additional Free services does not multiply the build allowance. Builds and pre-deploy execution consume build minutes. Queueing, cloning, and provisioning are excluded. Paid application services can contribute additional pooled minutes. Read [Builds](/deployments/builds) for allowance behaviour. ## When to upgrade [#when-to-upgrade] Choose paid compute when the application needs predictable availability, more CPU or memory, persistent uploads, shell access, or production deployment controls. Upgrade MySQL before its disk or connection limit becomes a bottleneck. Upgrades require your explicit selection and payment. Check the [current plan table](/billing/pricing) and the quote in the dashboard before confirming. # Get started with Openstead (/getting-started/overview) Openstead runs applications and databases on managed infrastructure. Connect your code, choose a service, and deploy. You do not need to supply a server, install a control panel, or maintain the underlying operating system. ## Choose your starting point [#choose-your-starting-point] | You want to run | Create | | --------------------------------------------------------------------------- | ---------------------------------------------------------------- | | An API, Laravel application, Django project, or server-rendered Next.js app | A [web service](/services/web-services) | | A built frontend, portfolio, or documentation site with no server process | A [static site](/services/static-sites) | | An internal HTTP or TCP application | A [private service](/services/private-services) | | A queue consumer or continuously running process | A [background worker](/services/background-workers) | | A command on a schedule | A [cron job](/services/cron-jobs) | | Relational application data | [PostgreSQL](/databases/postgresql) or [MySQL](/databases/mysql) | | A Redis-compatible cache or queue | [Key Value](/databases/key-value) | For a first deployment, follow [Deploy your first application](/getting-started/first-deploy). For an existing application, use the [framework guides](/guides/nextjs) and choose the guide that matches its runtime. ## How Openstead organises your work [#how-openstead-organises-your-work] A **workspace** contains your team's services, membership, billing, and integrations. A **project** groups related services. An **environment** separates a project's production and staging resources. A **service** is one independently configured application process or database. For example, one project might contain an API web service, a queue worker, and a PostgreSQL database in its Production environment. Put the staging application and its own database in a separate Staging environment. Applications connect to managed databases through private addresses in the same environment. Your browser connects to a public web service or static site using its Openstead URL or a [custom domain](/networking/custom-domains). ## Before you deploy [#before-you-deploy] * Create an Openstead account and verify your email address. * Push application code to a GitHub repository that you can authorise, or prepare a container image. * Know the directory containing the application, especially in a monorepo. * Collect the environment variables your application needs. Keep credentials out of Git. * Decide whether the application needs a database, a persistent upload directory, or an always-running instance. Openstead detects supported applications when you select a repository. Review the detected build command, start command, root directory, and port before starting a deployment. Detection saves configuration time; it does not replace application-specific setup. ## Start free or choose paid compute [#start-free-or-choose-paid-compute] Static sites, Free web instances, and MySQL Free offer different ways to get started. They have different resource limits and operating behaviour; see [Free instances](/getting-started/free-instances). Paid services use a calendar month purchased in advance. Review the total before checkout, including any application disk or additional replicas. There is no separate workspace subscription. See [Billing](/billing/overview). ## After your first deployment [#after-your-first-deployment] Check the service URL, review its logs, and test the paths that matter to your application. Then connect a domain, set up backups for any database, and invite teammates with the access they need. If something fails, [deployment troubleshooting](/guides/troubleshooting) explains where to look and what to include when contacting [support](https://openstead.tech/contact). # Deploy Django (/guides/django) Deploy Django as a web service. Use a production WSGI or ASGI server, a managed database for durable data, and a separate strategy for static assets and user uploads. ## Prepare production dependencies [#prepare-production-dependencies] Declare Django and a production server such as Gunicorn in your dependency manifest. For PostgreSQL, include a compatible PostgreSQL driver. You can use `dj-database-url` to parse a connection URL and WhiteNoise to serve collected static assets. Commit the dependency manifest and lockfile used by your chosen package manager. Keep `.env`, virtual environments, and local SQLite database files out of the deployment repository. ## Configure Django settings [#configure-django-settings] Your settings module must read the variables you configure. For example: ```python import os import dj_database_url DEBUG = False SECRET_KEY = os.environ["SECRET_KEY"] ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",") CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",") CSRF_TRUSTED_ORIGINS = [origin for origin in CSRF_TRUSTED_ORIGINS if origin] DATABASES = {"default": dj_database_url.config(conn_max_age=60)} STATIC_URL = "/static/" STATIC_ROOT = BASE_DIR / "staticfiles" SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True ``` This snippet assumes your settings already define `BASE_DIR`. Set `ALLOWED_HOSTS` to the generated hostname and any custom hostnames without schemes. Set `CSRF_TRUSTED_ORIGINS` to the HTTPS origins that submit browser requests to Django. Avoid using `*` for normal application host validation. If you use WhiteNoise, add its middleware immediately after Django's security middleware and configure its static storage backend for your installed Django version. ## Create the database and web service [#create-the-database-and-web-service] Create [PostgreSQL](/databases/postgresql) in the intended network scope. Put its private connection URL in `DATABASE_URL` on the web service, along with a strong, stable `SECRET_KEY` and the host settings above. For a project whose WSGI module is `config/wsgi.py`: | Setting | Value | | ------------------ | -------------------------------------------------------------------------------------------- | | Service type | Web Service | | Build method | Railpack | | Build command | `python manage.py collectstatic --noinput` | | Pre-deploy command | `python manage.py migrate --noinput` | | Start command | `gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --access-logfile - --error-logfile -` | | Port | `8000` | Replace `config` with your actual Python package. The builder prepares dependencies from the manifest. The pre-deploy command requires a paid instance and can reach the private database. Do not migrate the production database during image compilation. ## Add a readiness endpoint [#add-a-readiness-endpoint] The platform's readiness probe reaches the instance directly. With strict host validation, implement a narrowly scoped health middleware before middleware that enforces host or HTTPS redirects. For example: ```python # config/health.py from django.http import HttpResponse class HealthCheckMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): if request.path == "/health" and request.method == "GET": return HttpResponse("ok", content_type="text/plain") return self.get_response(request) ``` Add `config.health.HealthCheckMiddleware` at the beginning of the existing `MIDDLEWARE` list and set the service's health path to `/health`. This exposes only a minimal health response while normal routes keep their host and security checks. Add a fast bounded dependency check if your readiness requirements need more than application startup. ## Handle media and background tasks [#handle-media-and-background-tasks] Collected static files are build output. User-uploaded media must use object storage or a [persistent disk](/databases/persistent-disks) mounted at your configured `MEDIA_ROOT`. WhiteNoise is for static assets, not a complete user-media storage strategy. Deploy Celery as a separate [background worker](/services/background-workers) with the same application code and appropriate variables. Use shared queue and result-store connections as required. After deploying, run `python manage.py check --deploy` through a paid one-off job and verify login, forms, static assets, and database writes. Resolve its findings based on your actual proxy and application settings. # Deploy FastAPI (/guides/fastapi) FastAPI runs as a web service using an ASGI server. This example assumes the application object is `app` in `main.py`. ## Prepare the application [#prepare-the-application] Declare FastAPI and Uvicorn in your production dependencies, for example through `requirements.txt` or `pyproject.toml`. Add a simple health route: ```python from fastapi import FastAPI app = FastAPI() @app.get("/health") def health(): return {"status": "ok"} @app.get("/") def root(): return {"message": "Hello from Openstead"} ``` Commit your manifest and lockfile. Test the app locally with the same import path that the production command will use. ## Create a web service [#create-a-web-service] | Setting | Value | | ----------------- | ------------------------------------------------------------ | | Build method | Railpack | | Build command | Leave empty unless the application needs an extra build step | | Start command | `uvicorn main:app --host 0.0.0.0 --port $PORT` | | Port | `8000` | | Health check path | `/health` | For `src/api/main.py`, the correct module might be `api.main:app` with the appropriate working directory or `--app-dir src`. Adjust the import path to your package layout rather than keeping the detected default blindly. Do not use `--reload` in production. Begin with a worker count that fits the selected memory and CPU, then measure before increasing concurrency. ## Configure a database [#configure-a-database] Add the managed database's private connection details as environment variables. If you use an async driver, construct the URL in the format that driver expects; a generic PostgreSQL URL may need an application-level scheme adjustment for an async SQLAlchemy engine. For Alembic, use the appropriate release command, commonly: ```bash alembic upgrade head ``` Run it as a paid pre-deploy command. Do not create or migrate a production schema automatically inside every application worker's startup hook. ## Configure browser access [#configure-browser-access] Set CORS origins explicitly when a separate browser frontend calls the API. Keep private database credentials out of frontend configuration. If the API receives traffic through a trusted reverse proxy, configure Uvicorn's forwarded-header behavior deliberately for the ingress you actually use. FastAPI's generated OpenAPI and interactive documentation endpoints belong to your application. Decide whether those endpoints should be publicly accessible or protected; Openstead does not automatically apply your application's authorization to them. ## Verify the deployment [#verify-the-deployment] Check `/health`, an authenticated endpoint, and a database-backed operation. Look for import errors, missing production dependencies, and failed lifespan initialization in runtime logs. Long-lived job processing belongs in a separate worker rather than a background task that must survive an application restart. # Deploy Go (/guides/go) Openstead detects Go projects from `go.mod`. Commit `go.mod`, `go.sum`, and the source required to build the application. ## Bind the HTTP server [#bind-the-http-server] Use the runtime `PORT` variable and listen on all interfaces. A minimal server is: ```go package main import ( "log" "net/http" "os" ) func main() { port := os.Getenv("PORT") if port == "" { port = "8000" } mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) log.Fatal(http.ListenAndServe(":"+port, mux)) } ``` For your production application, add appropriate request timeouts, graceful shutdown, and application routes. ## Configure the service [#configure-the-service] For a repository with its main package in `cmd/server`: | Setting | Value | | ----------------- | ------------------------------------- | | Service type | Web Service | | Build method | Railpack | | Build command | `go build -o bin/server ./cmd/server` | | Start command | `./bin/server` | | Port | `8000` | | Health check path | `/health` | If the main package is in the root, build `.` instead. Declare a compatible Go version in `go.mod` and inspect the toolchain reported by the build. ## Custom native dependencies [#custom-native-dependencies] Applications using CGO or platform libraries may need a [Dockerfile](/deployments/docker). The runtime image must contain the native libraries required by the compiled binary; copying a dynamically linked binary into an unrelated minimal image can fail before the application logs anything. If you deliberately build a static binary, confirm that every dependency supports that choice. Include CA certificates in the runtime when the application makes HTTPS calls. ## Database connections and release work [#database-connections-and-release-work] Read the private database URL from an environment variable and configure a bounded connection pool. Use a dedicated migration executable or command in the paid pre-deploy phase if your application needs schema migrations. A binary must be executable and available at the configured start path. If the service fails immediately, inspect the build output path, file permissions, native library requirements, and runtime logs before changing networking settings. # Deploy Laravel (/guides/laravel) Laravel is a PHP web application even when it contains a Vite `package.json`. Openstead recognizes the Composer manifest and Laravel entry point before applying frontend-only detection. ## Select the correct application root [#select-the-correct-application-root] Choose the directory containing `composer.json` and `artisan`. For a standard Laravel application, the public document root is `public`. Some packaged PHP applications place Laravel in a nested directory while keeping public assets or `index.php` elsewhere. Preserve that layout and use a Dockerfile or appropriate server configuration when necessary. Moving only the framework folder can break asset paths, uploads, or the script's existing configuration. ## Use the automatic PHP build [#use-the-automatic-php-build] Select **Web Service** and **Railpack**. For a conventional Laravel layout, leave the build and start commands empty so the PHP provider handles Composer, browser assets, and the production server together. Do not replace that build with only `npm run build`. Declare required PHP extensions through Composer's `ext-*` requirements. Additional provider-supported extensions can be requested with `RAILPACK_PHP_EXTENSIONS`. Use a Dockerfile when you need exact PHP packages, production-only Composer installation, a custom public root, or other behavior beyond the provider's defaults. ## Configure application secrets [#configure-application-secrets] Add these variables to the service, using your actual values: ```text APP_ENV=production APP_DEBUG=false APP_URL=https://your-domain.example DB_CONNECTION=mysql DB_HOST= DB_PORT=3306 DB_DATABASE= DB_USERNAME= DB_PASSWORD= RAILPACK_SKIP_MIGRATIONS=true ``` Add a stable `APP_KEY` as a secret. For a new application, generate it once in a trusted local environment: ```bash php artisan key:generate --show ``` Do not generate a new key on every deployment. When migrating an existing application, preserve its existing key so encrypted data and sessions remain compatible. Enter values in Openstead's Environment page; do not commit the production `.env` file. ## Connect managed MySQL [#connect-managed-mysql] Create [MySQL](/databases/mysql) in the application's permitted private network. Copy its limited application credentials into the web service. `127.0.0.1` points to the application container itself, not a separate managed database. Set the paid service's **Pre-deploy command** to: ```bash php artisan migrate --force ``` The Railpack Laravel provider can run migrations during its default startup. `RAILPACK_SKIP_MIGRATIONS=true` prevents that behavior so the pre-deploy phase controls them once per release. Do not configure a pre-deploy command on a free web instance; choose a paid web plan for this production setup. ## Preserve uploads [#preserve-uploads] For a standard layout, a persistent disk mounted at `/app/storage` can retain local application data. Confirm where the actual script writes uploads: some packages use a different assets directory. Persist that exact directory or configure object storage. Initialize required storage subdirectories and permissions when the mounted disk is empty. Keep `bootstrap/cache` writable by the runtime user and regenerate configuration caches from runtime variables. Do not mount a data disk over your application source tree. If the app uses Laravel's public storage link, ensure the link is present in the runtime image or startup procedure. Database backups do not contain uploaded files; plan a separate backup for uploads. ## Queues and the scheduler [#queues-and-the-scheduler] Deploy queue consumption as a [background worker](/services/background-workers), for example: ```bash php artisan queue:work --sleep=3 --tries=3 --timeout=90 ``` Deploy the Laravel scheduler as a [cron job](/services/cron-jobs) with `* * * * *` and: ```bash php artisan schedule:run ``` Use the intended shared database or queue configuration. A worker or cron execution does not automatically share the web service's local upload disk, so jobs that need files should use shared object storage or another explicitly available store. ## Migrate an existing licensed script [#migrate-an-existing-licensed-script] Back up its database, uploaded files, original source, and configuration before migration. Preserve the application key and vendor-provided license or installation files. Use the vendor's supported domain-transfer procedure if the license is tied to a hostname; changing hosts does not justify removing the script's activation checks. Verify the intended domain, imported data, login, uploads, scheduled tasks, and a representative application action before switching traffic. Redeploy once and confirm that the same uploaded file and database record still exist. # Deploy Next.js (/guides/nextjs) Deploy Next.js as a **Web Service** when it needs server rendering, server-side route handlers, server actions, or other request-time behavior. Use a **Static Site** only when the application can be exported entirely as files. ## Prepare the application [#prepare-the-application] Commit `package.json`, the intended package-manager lockfile, and your application source. Ensure the production scripts exist: ```json { "scripts": { "dev": "next dev", "build": "next build", "start": "next start" } } ``` Use a Node.js version supported by your installed Next.js release and declare it in the project's runtime configuration. Run the production build locally before connecting the repository. ## Create a web service [#create-a-web-service] Connect GitHub and select the application root, then use these settings: | Setting | Value | | ------------- | -------------------------------------------------- | | Service type | Web Service | | Build method | Railpack | | Build command | `npm run build` | | Start command | `npm run start -- --hostname 0.0.0.0 --port $PORT` | | Port | `3000` | Replace npm with your project's package manager. Do not use `next dev` as the production start command. Deploy, open the assigned URL, and verify both a rendered page and a server-side route. ## Configure environment variables [#configure-environment-variables] Add server-only secrets such as `DATABASE_URL` in the service's Environment page. Keep them in server-side modules. Variables prefixed with `NEXT_PUBLIC_` are compiled into browser code and must contain only public values. Changing a public variable requires a new build. A server-only variable can also be evaluated at build time if a page is statically generated, so understand when the code reads it. Do not make `next build` depend on a private runtime database unless you have deliberately arranged a suitable build-time data source. Fetch request-specific or private database content at runtime. Run database migrations in a paid pre-deploy command. ## Optional standalone output [#optional-standalone-output] For a custom Docker image, `output: 'standalone'` can reduce runtime files. Next.js generates `.next/standalone/server.js` and the required traced dependencies. The standalone output does not automatically include `public` or `.next/static`. Copy those into the correct locations in the runtime image, set `HOSTNAME=0.0.0.0`, and start the generated `server.js`. A monorepo may also require a suitable `outputFileTracingRoot` so shared files are included. See [Dockerfiles](/deployments/docker) before replacing the default build path. Do not use `next start` as the startup command for a standalone-only image. ## Deploy a static export [#deploy-a-static-export] For a fully exportable application, use an ESM configuration such as: ```js // next.config.mjs export default { output: "export", trailingSlash: true, }; ``` Create a **Static Site**, run `npm run build`, and set the publish directory to `out`. A static export cannot run request-time server actions, server-only dynamic routes, or other features that need a server. The built-in image optimizer also requires an alternative configuration for static hosting. Test direct visits to nested routes. Choose a web service if an export limitation conflicts with your application instead of trying to run server code from a static directory. ## Production considerations [#production-considerations] Use external storage for uploads. If you add multiple replicas, review shared cache behavior, sessions, and release-specific secrets. Openstead deploys a Next.js Node server; it does not convert the application into Vercel's serverless or edge execution model. # Deploy Node.js and Express (/guides/nodejs) This guide uses Express, but the same web-service setup works for other Node.js HTTP frameworks when their production server binds to the configured port. ## Prepare the server [#prepare-the-server] For an ESM project with `"type": "module"` in `package.json`, a minimal `server.js` is: ```js import express from "express"; const app = express(); app.use(express.json()); app.get("/health", (_request, response) => { response.json({ status: "ok" }); }); app.get("/", (_request, response) => { response.json({ message: "Hello from Openstead" }); }); const port = Number(process.env.PORT || 3000); const server = app.listen(port, "0.0.0.0"); process.on("SIGTERM", () => { server.close(() => process.exit(0)); }); ``` For CommonJS, use `require("express")` instead of the import. Declare Express as a production dependency and commit the lockfile. Add a production start script: ```json { "scripts": { "start": "node server.js" } } ``` Merge this into the existing manifest rather than replacing its other fields. ## Deploy the service [#deploy-the-service] Create a **Web Service** from the repository: | Setting | Plain JavaScript | Compiled TypeScript | | ----------------- | --------------------------------- | ---------------------------------------------------- | | Build method | Railpack | Railpack | | Build command | Leave empty if no build is needed | Your build script, such as `npm run build` | | Start command | `npm run start` | Start compiled output, such as `node dist/server.js` | | Port | `3000` | `3000` | | Health check path | `/health` | Your implemented health route | Ensure the build emits the path used by the start command. Development tools such as nodemon should not be the production process. ## Connect a database [#connect-a-database] Create a managed database in the application's permitted private network and add its connection URL as a service variable. Use the database driver's connection pool, set sensible connection and query timeouts, and keep the total pool size within the database's limits as replicas increase. Prisma, Sequelize, Knex, and similar tools need their own schema migration commands. Put the appropriate release migration in a paid [pre-deploy command](/deployments/builds). Keep generated clients and production dependencies in the final artifact. ## Connect a separate frontend [#connect-a-separate-frontend] A browser-based frontend calls the API's public HTTPS URL. Configure CORS for the exact frontend origin and configure authentication cookies deliberately. A private database URL or private service hostname belongs only in backend code. ## Troubleshooting [#troubleshooting] If the release never becomes ready, check `0.0.0.0`, `PORT`, and the health path. If the runtime cannot find a module, confirm it is in production dependencies and was not omitted from the build. If an upload disappears after deployment, move it to a [persistent disk](/databases/persistent-disks) or object storage. # Platform status and incidents (/guides/platform-status) [Openstead System status](https://status.openstead.tech) provides availability information, incident updates, and scheduled maintenance notices. It is hosted separately from the Openstead console, so you can check it without signing in to your workspace. ## Read the affected components [#read-the-affected-components] Components separate the functions affected by an issue. Read the component name together with the incident description, affected scope, and timestamps. A problem creating new deployments does not necessarily interrupt applications that are already running. | Component | What the check covers | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dashboard | Access to the Openstead console. | | API | Availability of the platform API. | | Authentication API | Availability of the sign-in API. | | Deployment engine | Deployment-system connectivity and a recent processing heartbeat. | | Deployment queue | Whether eligible deployment work is being claimed and expired work can be recovered. | | Web service routing | HTTP routing to an Openstead-operated web service. | | Private PostgreSQL queries | Authenticated read-only queries to a dedicated PostgreSQL database over Openstead's private network. | | Private MySQL queries | Authenticated read-only queries to a dedicated MySQL database over Openstead's private network. | | Checkout processing | Live payment-provider access, webhook availability, and reconciliation of completed Openstead collections with payment confirmations and purchased entitlements. | | Website | Openstead's product information and pricing website. | | Documentation | Guides, API reference, and the product changelog. | A successful check confirms the particular function being checked. Deployment checks do not guarantee that an individual build completes, and the authentication check does not exercise every social sign-in flow. These checks do not establish that every customer application, database query, or payment works. Use your service's [logs and metrics](/deployments/logs) and [health checks](/deployments/health-checks) alongside platform updates. The deployment-queue check distinguishes an eligible backlog from expected waits for capacity, another operation on the same service, workspace concurrency, or a monthly build allowance. A build that is already running can take time without indicating a queue outage. Use the deployment's own state and logs to diagnose that build. The database checks use separate Openstead-operated databases, with private connections and restricted query accounts. They do not read customer data, expose database ports, or establish the condition of every customer's database. A successful query does not verify backups, restoration, or high availability. The checkout check observes live processing without charging a card. When Bachs reports a completed Openstead collection, the monitor checks that its identity, amount, and currency match Openstead's records, that the signed confirmation was processed, and that the purchased credit, build minutes, or service term was recorded. Delayed confirmations and missing entitlements trigger operator alerts after a processing allowance. An idle checkout system can be operational without any recent payments; this check does not guarantee that a bank will approve a card or exercise every checkout screen. ## Follow an incident [#follow-an-incident] Incident updates explain what customers are experiencing and how recovery is progressing. The usual stages are: | Stage | What it means | | ------------- | ------------------------------------------------------------------- | | Investigating | We are checking the reported impact and determining the cause. | | Identified | We have identified the cause and are working on a correction. | | Monitoring | A correction is in place and we are checking recovery. | | Resolved | We have verified that the affected platform function has recovered. | Follow any action described in the notice. Avoid making several unrelated configuration changes while investigating a platform incident. After recovery, verify your own application and retry a failed operation only when appropriate; a resolved incident does not replay every failed customer request. ## Check updates and incident history [#check-updates-and-incident-history] Open the status page to read the current component states, active notices, and incident history. Revisit the incident while it is being investigated for the latest verified impact and recovery update. You do not need an Openstead console session to read the page. Your workspace's [webhooks](/integrations/webhooks) and application alerts are separate. Workspace events describe your resources; the status page communicates Openstead platform availability. ## Check planned maintenance [#check-planned-maintenance] Maintenance notices identify the affected components, scheduled window and time zone, expected impact, and any action you need to take. Read progress updates to see when work begins and when it is complete. Do not assume that every maintenance window interrupts running applications. Your own application's [maintenance mode](/networking/access-controls#turn-on-maintenance-mode) is a separate routing setting. Enabling it does not publish an incident or maintenance notice on Openstead's platform status page. ## When only your application has a problem [#when-only-your-application-has-a-problem] An application can fail while the platform components are operational. Examples include a failed build command, an application exception, incorrect DNS, a missing environment variable, an exhausted connection pool, or a suspended service. A Free web instance waking from inactivity can also take time to respond. Start with [Troubleshoot a deployment](/guides/troubleshooting). Compare the incident's reported scope with your error, deployment state, and recent configuration changes. If several functions fail and no matching incident is listed, report what you observe so it can be investigated. ## Contact support [#contact-support] Contact [Openstead support](https://openstead.tech/contact) with the affected service or deployment ID, the time and time zone, the error or request ID, and the steps that produced the problem. Include a relevant status notice if one exists. Remove passwords, API keys, database credentials, payment details, and customer data from logs or screenshots. # Deploy Ruby and Rails (/guides/ruby) Openstead recognizes Ruby projects from `Gemfile`. Commit `Gemfile.lock` and your intended Ruby version configuration so the build uses a compatible runtime and reproducible dependencies. ## Prepare a Rails application [#prepare-a-rails-application] Confirm that the application includes its production server, commonly Puma, and a driver for the managed database you intend to use. Configure production credentials, database settings, asset storage, and allowed hosts for your application's Rails version. Add these environment values as appropriate: ```text RAILS_ENV=production RAILS_LOG_TO_STDOUT=true DATABASE_URL= ``` If the application uses encrypted Rails credentials, add its `RAILS_MASTER_KEY` as a secret. If it reads `SECRET_KEY_BASE` directly, configure that securely and preserve it between deployments. Do not commit production keys or assume that setting an unused environment variable changes your application configuration. ## Configure the web service [#configure-the-web-service] Choose **Web Service** and **Railpack**. A conventional Rails configuration can use: | Setting | Value | | ------------------ | ------------------------------------------------------------------------- | | Build command | `bundle exec rails assets:precompile` when the app serves compiled assets | | Pre-deploy command | `bundle exec rails db:migrate` | | Start command | `bundle exec rails server -b 0.0.0.0 -p $PORT` | | Port | `8000` | The pre-deploy command requires a paid instance. API-only Rails applications may not have an asset pipeline; omit the asset build in that case. Applications using a separate JavaScript asset tool must also run its required production build. Ensure the web server and required gems are available in the production bundle. Avoid initialization that queries the private runtime database while assets are being compiled in the isolated build environment. ## Health and host checks [#health-and-host-checks] Use the application's implemented health route, such as `/up` in Rails versions that provide it, and set that exact path in Openstead. Confirm it responds to a direct instance probe without login or a domain-only redirect. Do not assume every Rails version or application defines `/up`. Configure allowed hosts and trusted proxy handling for the generated hostname and custom domain. Keep production security checks on normal application routes. ## Files and background work [#files-and-background-work] Use object storage for Active Storage in applications that need multiple instances. A local persistent disk requires a paid service and one replica, and is not automatically shared with worker services. Deploy Sidekiq or another queue consumer as a separate [background worker](/services/background-workers). For Sidekiq, a common command is `bundle exec sidekiq`; supply the correct private Key Value connection and application variables. ## When to use Docker [#when-to-use-docker] Use a Dockerfile when the app needs a specific Ruby build, native system library, image-processing package, or a custom asset sequence. Rails-generated production Dockerfiles can be a useful starting point, but review the image's startup migrations, port, credentials, and storage assumptions before deploying. Validate the homepage or API, compiled assets, login, a database write, and a queued job after deployment. # Troubleshoot a deployment (/guides/troubleshooting) Start with the affected service and its most recent deployment. Record the deployment ID, status, error text, and time. Determine whether the problem occurred while building, while starting the application, or after a release was already live. ## Check platform status [#check-platform-status] Check [Openstead System status](https://status.openstead.tech) for an incident or maintenance notice that matches the affected function and time. A platform incident can affect new deployments while existing applications continue serving requests. Follow the notice for the current impact and updates. If no matching incident is listed, continue the checks below. An operational component does not guarantee that every application, dependency, or account is healthy. See [Platform status and incidents](/guides/platform-status) to follow updates or report an issue. ## Repository missing or GitHub access denied [#repository-missing-or-github-access-denied] Confirm that the connected GitHub App installation has access to the repository and that you selected the correct account or organization. A social login alone does not grant repository access. If an installation settings link returns 404, check the GitHub account and the current installation rather than repeatedly opening an old link. See [Connect GitHub](/deployments/github). ## Deployment remains queued [#deployment-remains-queued] Read the queued deployment's reason. It may be waiting for build concurrency, monthly build allowance, another operation on the same service, or available capacity. Complete a required payment through Billing if the service has no active paid month. Creating more deployment requests does not remove those requirements. ## Build fails [#build-fails] Locate the first meaningful error in the build log. Check: * The selected branch and application root. * The dependency manifest, lockfile, and supported language version. * Whether the build command creates the expected output. * Required build variables and system packages. * Whether the build incorrectly tries to use a private runtime database. For a static site, verify that **Publish directory** exists after the build. For Laravel, confirm that an asset-only Node build did not replace the PHP application's build. ## Build succeeds but readiness fails [#build-succeeds-but-readiness-fails] Check the runtime log and start command. The process must remain in the foreground, listen on `0.0.0.0`, and use the configured port. Confirm the health path returns a successful response without authentication or host-specific middleware rejection. An out-of-memory crash, missing production dependency, or database connection error can stop the process before it listens. See [Health checks](/deployments/health-checks). ## The browser shows 502 or 503 [#the-browser-shows-502-or-503] Check whether the service is live, deploying, sleeping, suspended, or marked as needing attention. Inspect runtime logs and the latest release. Free web instances can take time to wake after inactivity. A suspended service must be resumed after any blocking billing or configuration issue is resolved. If a platform error page includes a request ID, save it with the timestamp. If the application itself returns the error, investigate its own request logs as well. The HTTP status alone does not identify the root cause. ## Custom domain does not work [#custom-domain-does-not-work] Open the service's domain settings and use the exact current DNS instructions. Check the ownership TXT record, the routing record, conflicting A/AAAA records, and proxy settings. Verification and certificate issuance are separate steps; allow time for public DNS changes to propagate. Compare the generated Openstead URL with the custom hostname. If the generated URL works, focus on domain verification, DNS, TLS, and application allowed-host settings. See [Custom domains](/networking/custom-domains). ## Static pages or assets return 404 [#static-pages-or-assets-return-404] Confirm the publish directory and inspect the built file paths. A client-side router may need an `/index.html` rewrite for direct navigation. An incorrect asset base URL can instead make JavaScript or CSS requests miss their files. Test those asset URLs directly before adding broad rewrites. ## Database connection fails [#database-connection-fails] Confirm that the database is running and that the app is in the permitted private-network scope. Use the database's actual private host and port; `localhost` means the application itself. Verify the database name, application user, driver URL format, and connection-pool limits without printing the password. Private database addresses do not connect directly from a laptop. Use the supported management tools or an application inside the permitted network. ## Uploads disappear after deployment [#uploads-disappear-after-deployment] Determine the exact directory where the application writes files. Files in a container's normal writable layer do not survive replacement. Attach a persistent disk to the correct data path or use object storage, then test another redeployment. A database backup does not also back up an application's upload disk. ## Need to restore service quickly [#need-to-restore-service-quickly] If a previous release is compatible with the current data and credentials, consider a [rollback](/deployments/rollbacks). Avoid changing several unrelated settings at once; make one observable correction, deploy, and verify. For a service marked **Needs attention**, or a failure you cannot isolate, contact [Openstead support](https://openstead.tech/contact). Include the service and deployment IDs, approximate time and time zone, relevant error or request ID, and sanitized logs. Never include passwords, API tokens, private keys, or full environment dumps. # Deploy a Vite frontend (/guides/vite) A standard Vite frontend builds into static files. Deploy those files as a Static Site; you do not need a continuously running `vite preview` server. ## Prepare the repository [#prepare-the-repository] Commit your source, `package.json`, and package-manager lockfile. Ensure the build script invokes Vite, directly or after type checking: ```json { "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" } } ``` Keep your existing TypeScript checks if your project already includes them. Run the build locally and confirm that its output directory contains `index.html` and the generated assets. ## Create a static site [#create-a-static-site] | Setting | Value | | ----------------- | --------------- | | Service type | Static Site | | Build method | Railpack | | Build command | `npm run build` | | Publish directory | `dist` | | Start command | Leave empty | Use your actual package manager and configured `build.outDir` if they differ. Select the correct root in a monorepo so the manifest and lockfile are available. ## Configure the API URL [#configure-the-api-url] For a browser application calling a separate API, add a public build variable such as: ```text VITE_API_URL=https://api.example.com ``` Read it in browser code: ```js const response = await fetch(`${import.meta.env.VITE_API_URL}/products`); ``` The API must expose a public HTTPS endpoint and allow the frontend's origin when CORS applies. Do not point browser code at a private database or private-service hostname. Every `VITE_*` value used by the frontend is public build output. Never put database passwords or backend API secrets in those variables. After changing a value, deploy again so Vite can compile it into the site. ## Support client-side routing [#support-client-side-routing] If you use React Router, Vue Router, or another history-based browser router, add a `/*` to `/index.html` rewrite with action `200` in **Redirects & rewrites**. Deploy the change, then open a nested route directly in a new tab. If JavaScript or CSS returns the HTML page instead of the expected asset, inspect its path and Vite's `base` setting. Do not hide missing assets behind a blanket fallback without checking the browser's network errors. ## Connect a domain and verify [#connect-a-domain-and-verify] Follow [Custom domains](/networking/custom-domains), then verify the homepage, a nested route, a direct asset URL, and API calls from the final origin. If authentication uses cookies, verify its domain, SameSite policy, HTTPS requirements, and CORS credentials configuration on the API. Vite can also be part of an SSR framework. If your output needs a Node server, deploy the framework's production server as a [web service](/services/web-services) instead of using these static-only settings. # Openstead Documentation (/) Deploy your applications, connect your data, and keep everything running. Openstead hosts your applications and managed databases. Bring a connected repository or container image; Openstead operates the infrastructure. Start with the first deployment guide, choose a service for each part of your application, and configure variables, networking, and storage before operating it in production. ## Get started - [Get started with Openstead](/getting-started/overview): Understand your workspace, choose the right service, and bring your first application online. - [Deploy your first application](/getting-started/first-deploy): Connect GitHub, review detected settings, and deploy a web application or static site. - [Free instances](/getting-started/free-instances): Understand free web hosting, static sites, MySQL Free, and the limits each includes. - [Frequently asked questions](/getting-started/faq): Answers about servers, GitHub, databases, domains, deployment, and payments. - [Changelog](/changelog): The latest additions, improvements, and fixes across Openstead. Your weekly look at what’s new. ## Deploy applications and databases - [Choose a service](/services/overview): Match each part of your application to the right Openstead service. - [Web services](/services/web-services): Deploy APIs, application backends, and server-rendered websites from GitHub or a container image. - [Static sites](/services/static-sites): Publish a frontend or content site as versioned HTML, CSS, JavaScript, and other assets. - [Private services](/services/private-services): Run internal application services without exposing a public website. - [Background workers](/services/background-workers): Process queues and long-running asynchronous work outside your HTTP server. - [Cron jobs](/services/cron-jobs): Build a command once and run it on a schedule with execution history and logs. - [PostgreSQL](/databases/postgresql): Create a managed PostgreSQL database, connect privately, and protect application data with backups. - [MySQL](/databases/mysql): Deploy MySQL 8.4, use the free tier, and connect Laravel or other applications over the private network. - [phpMyAdmin](/databases/phpmyadmin): Browse, query, import, and export your MySQL database securely through the Openstead dashboard. - [Key Value](/databases/key-value): Use a private Redis-compatible service for caching, queues, and shared application state. - [Persistent disks](/databases/persistent-disks): Keep application uploads and local data across deployments with a mounted persistent disk. ## Configure your stack - [Projects and environments](/account/projects): Group related services and separate production, staging, and preview resources. - [Connect GitHub](/deployments/github): Select repositories, configure access, and deploy commits automatically. - [Builds and commands](/deployments/builds): Configure build, pre-deploy, and start commands and understand caching and build minutes. - [Environment variables and secrets](/deployments/environment-variables): Configure application secrets, shared environment groups, and mounted secret files. - [Monorepos](/deployments/monorepos): Deploy several applications from one repository with separate roots, commands, and release policies. - [Blueprints](/blueprints/overview): Define related Openstead services in a YAML file and manage them as one project. - [Blueprint YAML reference](/blueprints/reference): Supported manifest fields, service types, configuration mappings, and validation rules. - [Custom domains](/networking/custom-domains): Connect your own hostname to a web service or static site with automatic DNS verification and HTTPS. - [Cloudflare DNS](/networking/cloudflare): Set up Cloudflare records for an Openstead custom domain and resolve verification problems. - [Private networking](/networking/private-networking): Connect services and databases within an environment without exposing internal endpoints to the internet. - [HTTPS and certificates](/networking/https): Understand automatic certificates, domain readiness, and HTTPS troubleshooting. ## Operate and manage - [Logs and metrics](/deployments/logs): Read deployment output, inspect runtime logs, and use resource measurements to investigate problems. - [Health checks](/deployments/health-checks): Tell Openstead when a web or private service is ready to receive requests. - [Rollbacks and recovery](/deployments/rollbacks): Restore an earlier successful release while accounting for database and configuration changes. - [Backups and recovery](/databases/backups): Create database exports, configure retention, and restore into a separate database without overwriting the source. - [Troubleshoot a deployment](/guides/troubleshooting): Diagnose repository, build, readiness, domain, data, and runtime failures in a repeatable order. - [Platform status and incidents](/guides/platform-status): Check Openstead service availability, follow incident updates, and distinguish platform issues from application-specific failures. - [Team members and roles](/account/team-members): Invite collaborators, grant appropriate workspace roles, and transfer ownership safely. - [Billing overview](/billing/overview): Understand monthly service purchases, checkout, promotional credit, and workspace access. - [Account security](/account/security): Manage sign-in methods, email verification, two-factor authentication, recovery codes, and sessions. ## Developer tools - [API overview](/api/overview): Automate projects, services, variables, deployments, and logs with the Openstead REST API. - [API reference](/api/reference): Explore every operation in the Openstead public API. - [Python SDK](/integrations/python-sdk): Use synchronous and asynchronous typed Python clients for the Openstead core API. - [TypeScript SDK](/integrations/typescript-sdk): Call the Openstead core API from Node.js with typed resources, async iterators, and cancellation. - [Openstead CLI](/integrations/cli): Deploy, inspect, and operate your applications from Windows, macOS, Linux, or CI. - [MCP server](/integrations/mcp): Connect an MCP-compatible assistant to Openstead with explicit workspace and tool permissions. ## Framework quickstarts - [Deploy Django](/guides/django): Deploy a Django application with Gunicorn, private PostgreSQL, static assets, and release migrations. - [Deploy FastAPI](/guides/fastapi): Run an ASGI API with Uvicorn and connect it to private application data. - [Deploy Go](/guides/go): Build and run a Go HTTP service with an explicit entry point and listening port. - [Deploy Laravel](/guides/laravel): Deploy Laravel with managed MySQL, persistent uploads, controlled migrations, queues, and scheduled tasks. - [Deploy Next.js](/guides/nextjs): Run Next.js as a Node.js web service or publish a fully static export. - [Deploy Node.js and Express](/guides/nodejs): Deploy a Node.js API with a production start command and a reachable HTTP port. - [Deploy Ruby and Rails](/guides/ruby): Deploy a Ruby web application with a production server, database, assets, and background workers. - [Deploy a Vite frontend](/guides/vite): Publish a Vite application as a static site and connect it to a separate API. ## Get help Check [System status](https://status.openstead.tech) for known incidents and read how to [follow service updates](/guides/platform-status). Read the [FAQ](/getting-started/faq) or contact [Openstead support](https://openstead.tech/contact). The [documentation index](/llms.txt) lists every page. [All documentation as Markdown](/llms-full.txt) includes guides and the public API reference. # CLI command reference (/integrations/cli-reference) Run `openstead --help` or `openstead COMMAND --help` for the exact options supported by your installed release. ## Command groups [#command-groups] | Command | Purpose | | ----------------------------------------------------------------- | ----------------------------------------------------------- | | `login`, `logout`, `whoami`, `workspaces`, `link` | Credentials, workspace profiles, and local service context. | | `catalog` | Available plans, runtimes, and capabilities. | | `services`, `projects`, `environments`, `groups` | Resource configuration. | | `deploy`, `deploys` | Queue, inspect, wait, cancel, or roll back deployments. | | `status`, `logs`, `metrics` | Runtime state and diagnostic output. | | `restart`, `suspend`, `resume`, `open` | Operate a service or open its public URL. | | `env`, `secret-files` | Manage encrypted application secrets. | | `shell`, `jobs` | Interactive terminal and one-off commands. | | `domains`, `disks`, `headers`, `redirects`, `schedules` | Service resource settings. | | `backups` | Queue, list, download, delete, or restore database backups. | | `github` | Repository access, branches, and framework detection. | | `blueprints` | Saved YAML, validation, and project creation. | | `integrations`, `registries`, `connections`, `routing`, `scaling` | Integrations and runtime information. | | `workspace`, `members`, `invitations`, `audit`, `notifications` | Workspace administration and history. | | `usage`, `billing` | Consumption, invoices, and dashboard checkout. | | `completion` | Bash, zsh, fish, and PowerShell completions. | All operations use the caller's permissions and service entitlements. Some non-core lists return only their latest records; GitHub listings support `--page`, with `nextPage` in JSON output. ## Common context [#common-context] | Flag | Environment variable | Purpose | | ------------------- | ---------------------- | ------------------------------------------------- | | `--workspace`, `-w` | `OPENSTEAD_WORKSPACE` | Workspace UUID. | | `--service`, `-s` | `OPENSTEAD_SERVICE` | Service name or UUID for commands that accept it. | | `--profile` | `OPENSTEAD_PROFILE` | Saved authorization profile. | | `--api-url` | `OPENSTEAD_API_URL` | API origin. | | `--token-file` | `OPENSTEAD_TOKEN_FILE` | Private credential file. | | `--json` | — | Machine-readable output. | | `--yes`, `-y` | — | Approve the command's confirmation. | `OPENSTEAD_API_KEY` supplies a key directly. A token file takes precedence over that environment key, which takes precedence over the keychain. For workspace/service context, explicit flags and environment values precede local `openstead.toml`, then the active profile. `OPENSTEAD_CONFIG_DIR` can select another CLI configuration directory. The corresponding `RUNIVO_*` environment names remain supported as fallbacks. Existing `runivo.toml` files are read when `openstead.toml` is absent. The default API origin is `https://openstead-dashboard.vercel.app`; the CLI uses its API proxy. SDK base URLs include `/api/v1`, while this CLI option is an origin. CLI profiles remain pinned to the origin where they were authorized. You can select another origin with `--api-url`, but saved credentials are never forwarded to a different origin automatically. ## Create and update resources [#create-and-update-resources] ```bash openstead services create --file service.json openstead services update example-api --file service-update.json openstead projects create --name production openstead domains create --service example-api --name app.example.com openstead domains verify app.example.com --service example-api --yes ``` `--file` accepts the operation's JSON request body; `--file -` reads stdin. Service creation saves configuration. Add `--deploy` to request an initial release, subject to payment, quota, and runtime requirements. ## Supply secrets [#supply-secrets] Use a private file or stdin so values do not become command-line arguments: ```bash openstead env set API_TOKEN --file ./api-token.txt --service example-api openstead env import --file variables.json --service example-api --yes openstead secret-files set credentials.json --file ./credentials.json --service example-api ``` These file-based commands work across supported shells. You can also pipe the exact intended bytes to `env set`. Secret input preserves trailing newlines. Imports apply one key at a time and can partially succeed; inspect the command result before retrying. To target environment-group secrets, use `env --group NAME_OR_ID`. ## Observe and run commands [#observe-and-run-commands] ```bash openstead logs --service example-api --follow openstead shell --service example-api openstead jobs run --service example-worker --command 'python manage.py check' --wait --yes ``` Shell access and jobs follow service entitlements. Disconnect an interactive shell with Ctrl+]. A one-off command runs inside your workload and can affect its data. ## Output and exit codes [#output-and-exit-codes] JSON goes to stdout; errors and progress go to stderr. Follow and watch commands emit newline-delimited JSON. The CLI does not automatically retry a mutation after an uncertain network result; inspect the target resource first. | Exit | Meaning | | ---- | --------------------------------------------------------------------------------------- | | 0 | Command succeeded; asynchronous work may still be queued unless observed with `--wait`. | | 1 | Request, network, or other error. | | 2 | Confirmation declined. | | 3 | Authentication required or expired. | | 4 | Permission or entitlement denied. | | 5 | Resource not found. | | 6 | Conflict. | | 7 | Rate limited. | | 8 | Deployment wait timed out. | | 9 | Deployment or job failed, was cancelled, or was superseded. | | 130 | Interrupted. | ## Raw workspace API calls [#raw-workspace-api-calls] `openstead api PATH` sends a request relative to the current authorized workspace. It does not bypass access checks. Use [the published core reference](/api/reference) for REST integrations that need the stable public contract; prefer dedicated CLI commands for other console workflows. # Openstead CLI (/integrations/cli) The Openstead CLI uses your workspace's permissions and plan limits. It deploys connected repositories and configured container images; it does not upload a local source directory. ## Install [#install] Build the Openstead CLI from the [source repository](https://github.com/Layerrail/openstead-cli) with Go **1.27 or newer**: ```bash git clone https://github.com/Layerrail/openstead-cli.git cd openstead-cli git checkout f79730ed7ca9f207145f2b1de80d49d529e211e3 go build -o openstead ./cmd/openstead ``` On Windows, use `go build -o openstead.exe ./cmd/openstead`. Place the resulting executable on your `PATH`, then run `openstead --version`. Use a reviewed source commit for reproducible production and CI installations. The examples below use the Openstead source version. Older release archives retain their original executable names. Existing `runivo` commands and configuration remain supported; new source builds also provide the `openstead` entry point. ## Sign in [#sign-in] ```bash openstead login openstead whoami openstead services list ``` Login opens a browser approval page. Check its displayed code, choose a workspace, and approve access. The CLI stores the resulting key in your OS keychain. Login keys expire after 30 days. Use `openstead login --read-only` for observation or `--no-browser` when signing in from a remote machine. Linux keychain storage requires Secret Service. On a headless machine, explicitly use a private token file: ```bash openstead login --no-browser --token-file /private/path/openstead-token openstead whoami --token-file /private/path/openstead-token ``` Keep that file readable only by its owner. Use the same token-file option on later commands. ## Link and deploy a service [#link-and-deploy-a-service] ```bash openstead link example-api openstead deploy --wait openstead logs --follow ``` `link` writes IDs to `openstead.toml` in the current directory. It does not change Git configuration or upload files. `deploy` builds the service's configured repository branch or image. To target a service explicitly: ```bash openstead deploy --service SERVICE_UUID --wait openstead status --service SERVICE_UUID --watch ``` A timeout stops the local wait. Resume observation with `openstead deploys wait DEPLOYMENT_UUID`; do not assume the deployment stopped. ## Work with multiple workspaces [#work-with-multiple-workspaces] ```bash openstead login --profile production openstead login --read-only --profile staging openstead workspaces list openstead workspaces use production ``` Each profile authorizes one workspace. `workspaces list` shows locally authorized profiles. Saved credentials are bound to their API origin and are not forwarded to a different origin. ## Use in CI [#use-in-ci] Install a verified CLI release in the runner. Put `OPENSTEAD_API_KEY` in the CI system's masked secret store and set `OPENSTEAD_WORKSPACE` and `OPENSTEAD_SERVICE` to the intended UUIDs. ```bash openstead deploy --wait --json ``` The CLI reads these environment values without browser login. Use `--yes` only where the pipeline intentionally approves a command that requires confirmation. Paid service terms must be purchased through the dashboard; `openstead billing open` opens that flow. ## Sign out [#sign-out] ```bash openstead logout --yes ``` You can also revoke a credential in the dashboard's API Keys page. See the [command reference](/integrations/cli-reference) for resource commands, exit codes, and context precedence. # Private container registries (/integrations/container-registries) Connect a container registry when your service deploys an image that requires authentication. Credentials are scoped to the workspace and kept encrypted. ## Requirements [#requirements] * An owner or admin account in the workspace. * An authorized, deployed paid service that enables private registry access. * A public HTTPS registry on port 443. * A registry username and read-only token permitted to pull the image. Openstead accepts registry hostnames rather than IP addresses, localhost, or private-network names. Supported endpoint shapes include the HTTPS origin and an optional `/v2/` suffix. ## Add credentials [#add-credentials] 1. Open **Container Registry Credentials** in the dashboard. 2. Select **Add Credential** and enter a descriptive name. 3. Enter the registry URL, such as `https://ghcr.io`. 4. Enter the registry username and a pull-scoped token. 5. Enable **Allow use for image deployments** and save. The saved token is hidden. Leave the credential field blank during an edit to retain it. Changing the registry host or username requires a new credential. ## Deploy an image [#deploy-an-image] Create or edit a supported service with an image source. Enter an image reference such as: ```text ghcr.io/example/example-api:2026-09 ``` Choose the registry credential saved in this workspace. Set the container's application port and any required variables, complete checkout for the service if needed, then deploy. The equivalent service configuration fields are: ```json { "sourceType": "image", "buildMethod": "image", "image": "ghcr.io/example/example-api:2026-09", "registryId": "00000000-0000-4000-8000-000000000001", "port": 8000 } ``` Replace the example `registryId` with the actual UUID returned for your saved registry. The selected credential's host must match the image host. A mismatch is rejected before credentials are sent. ## Release and credential management [#release-and-credential-management] Use immutable tags or digests for reproducible releases. A moving tag can resolve to different images on later deployments. Release snapshots retain encrypted registry credentials for rollback. If a registry token is revoked, an older release may no longer be pullable. Rotate credentials, deploy successfully with the replacement, and check rollback requirements before removing access to old images. Remove registry references from active service configurations before deleting the credential. Disabling a credential prevents its use for new image deployments. ## Troubleshooting [#troubleshooting] | Problem | Check | | ---------------------------------------- | ---------------------------------------------------------------------------------- | | Registry cannot be selected | Workspace entitlement, credential enabled state, and current role. | | Host mismatch | Image hostname and saved registry URL, including Docker Hub aliases. | | Image pull denied | Token pull permissions, organization access, repository visibility, and image tag. | | Container starts but fails health checks | Application port, bind address, required variables, and runtime logs. | For a source repository, use [GitHub deployments](/deployments/github) instead of storing a GitHub source-access credential in the registry form. # Log and metric streams (/integrations/log-and-metric-streams) Openstead can send JSON batches of logs or runtime metrics to a workspace's HTTPS destination. Open **Observability** in the dashboard as an owner or admin. ## Configure a destination [#configure-a-destination] 1. Choose **Log Streams** or **Metrics Stream**. 2. Add the destination's public HTTPS URL on port 443. 3. Supply an access token if your collector expects Bearer authentication. 4. Save the enabled connection and send a delivery test. Metrics streaming requires an authorized, deployed paid service in the workspace. Openstead forwards its JSON format; use a collector or adapter when your monitoring provider expects a different ingestion protocol. ## Batch format [#batch-format] Both stream types use the webhook envelope with `id`, `type`, `version`, `createdAt`, `workspaceId`, and `data.records`. `logs.batch` records contain: | Field | Meaning | | -------------------------- | --------------------------------- | | `id` | Numeric source log identifier. | | `serviceId`, `serviceName` | Originating service. | | `timestamp` | Log timestamp. | | `source` | Build, runtime, or system source. | | `level` | Reported log level. | | `message` | Application output. | `metrics.batch` records contain the same service identity and timestamp, plus `cpuPercent`, `memoryBytes`, `memoryLimitBytes`, `networkReceivedBytes`, `networkSentBytes`, and `replicas`. Network values are container counters that include private traffic. Do not treat them as a public bandwidth invoice. Handle counter resets when containers restart. ## Delivery behavior [#delivery-behavior] Streams begin at activation and send future data. New or re-enabled destinations do not backfill older records. Preview-log forwarding follows workspace preferences. Delivery uses a durable queue and retries failures up to eight attempts. Deduplicate by the delivery ID because an accepted batch may be delivered again. Return 2xx responses only after safely accepting the batch. Collection pauses when a destination reaches 100 outstanding batches. Fix receiver failures promptly and use the delivery history to retry failed batches. Source retention is finite; a prolonged outage can outlast it. Editing or disabling a destination cancels pending deliveries. Review the endpoint before saving changes because an in-flight request can still reach its earlier destination. ## Protect application data [#protect-application-data] Log messages can contain sensitive values emitted by your application. Restrict collector access, avoid logging secrets in the application, and apply your own retention policy downstream. For an interactive retained-log reader, use the [Logs API](/api/logs) or CLI instead. # MCP server (/integrations/mcp) Openstead MCP is a local **stdio server** for Windows, macOS, and Linux. An MCP host launches the executable and communicates with it locally. It is not a remote hosted MCP URL. ## Install and check [#install-and-check] Build the server from the [MCP source repository](https://github.com/Layerrail/openstead-mcp) with Go **1.27 or newer**: ```bash git clone https://github.com/Layerrail/openstead-mcp.git cd openstead-mcp git checkout aa05b1f188141eabb1a4a48c64b5ce9c9b0c5026 go build -o openstead-mcp ./cmd/openstead-mcp ``` On Windows, use `go build -o openstead-mcp.exe ./cmd/openstead-mcp`. Pin a reviewed source commit when building production tools. The examples below use the Openstead source version; older release archives retain their original executable names. With the [Openstead CLI](/integrations/cli) installed, authorize a read-only workspace profile: ```bash openstead login --read-only --profile assistant openstead-mcp --profile assistant --check ``` ## Add the server to your MCP host [#add-the-server-to-your-mcp-host] For a host that uses an `mcpServers` configuration object: ```json { "mcpServers": { "openstead": { "command": "/absolute/path/to/openstead-mcp", "args": ["--profile", "assistant"] } } } ``` On Windows, use an absolute path such as `C:\\Tools\\openstead-mcp.exe`. The configuration file location depends on the host. Reconnect or restart the host after saving. For headless use, supply `OPENSTEAD_API_KEY` through the host's secret facility, or add `--token-file` and a private path to the arguments. Do not put an actual key in a shared JSON example or prompt. `--workspace WORKSPACE_UUID` can require a matching workspace identity. ## Read tools [#read-tools] Read tools are enabled by default. They cover services, projects, environments, deployments, bounded waits, logs, metrics, backup history, domains, integrations, variables' metadata, usage, and billing records available to the key. Tool names begin with `openstead_`; the host discovers their schemas. Existing `runivo_` tool names and `runivo://` resource URIs remain compatibility aliases. The server also accepts `RUNIVO_*` environment names as fallbacks. Example requests: * “Show my services and identify failed deployments.” * “Read the latest build logs for my API and explain the failure.” * “Show the DNS records required for this custom domain.” * “Check my build usage and unbilled charges.” Results include `ok`, `workspaceId`, and either `data` or `error`. A queued result is not evidence of a completed deployment. A wait observes for at most 30 seconds and returns `completed: false` if the work is still running. Check `outcome` when it completes. ## Enable changes [#enable-changes] Create a write-scoped profile, then enable writes explicitly when starting the server: ```bash openstead login --profile operator openstead-mcp --profile operator --allow-writes --check ``` Use `["--profile", "operator", "--allow-writes"]` in the host's server arguments. This exposes supported resource changes, deployment controls, domain changes, and backup creation. Configure the host to request approval for changes. Tool annotations and name-confirmation fields do not replace the caller's authorization. Two optional capabilities require `--allow-writes` as well: | Flag | Enables | | ----------------- | ----------------------------------------------- | | `--allow-exec` | One-off workload jobs and cancellation. | | `--allow-secrets` | Setting and deleting variables or secret files. | Secret values passed through an assistant can appear in its transcript. There is no secret-reveal tool. Read-only API credentials remain read-only regardless of startup flags. Core writes require a `request_id`; reuse it for an identical retry within the API's 24-hour receipt window. Mutations are not automatically retried. For non-core operations without replay guarantees, inspect state after a lost response. ## Boundaries [#boundaries] Creating a service through MCP saves configuration. Paid capacity needs an authorized term in the dashboard before deployment. The MCP server does not charge cards or issue credits. Interactive terminals, database restore/download flows, and account administration use the dashboard or CLI. MCP tools do not execute arbitrary commands on the computer running the host. An allowed workload job runs in the selected Openstead service. API results and logs are data to inspect, not instructions for the assistant to follow. Choose the minimum workspace scope needed, review changes before approving them, and revoke the profile's key when access is no longer needed. # Slack and Discord notifications (/integrations/notifications) Connect Slack or Discord incoming webhooks from the dashboard's **Notifications** page. An owner or admin configures these workspace integrations. ## Connect Slack [#connect-slack] 1. Create an incoming webhook in your Slack workspace and select the destination channel. 2. In Openstead, choose **Connect Slack**. 3. Enter a name and the Slack incoming webhook URL. 4. Save the enabled connection. 5. Open its delivery history and send a test. Slack URLs must use `https://hooks.slack.com/services/...`. Use the webhook URL rather than a channel URL or bot token. Treat the webhook URL as a credential. ## Connect Discord [#connect-discord] Create a webhook in the target Discord channel's integration settings, copy its URL, then save it as a Discord connection in Openstead. Discord URLs must use `https://discord.com/api/webhooks/...`. Test the connection after saving. Notifications are sent without activating mentions. ## Events and delivery [#events-and-delivery] Channel integrations receive deployment success and failure, service health, and backup events. Messages contain the event title and description. Use [custom webhooks](/integrations/webhooks) when your receiver needs the structured JSON envelope and explicit event subscriptions. Saving an enabled connection begins delivery of future events. Historical messages are not replayed. Delivery retries up to eight attempts when the provider returns a failure or cannot be reached. ## Troubleshooting [#troubleshooting] | Symptom | Action | | -------------------------------- | ------------------------------------------------------------------------------- | | The URL is rejected | Use the provider's incoming-webhook URL on its supported host. | | A test fails | Check that the webhook is active and can post to the selected channel. | | HTTP 403 or 404 | The destination may have been revoked or removed; generate a replacement. | | HTTP 429 | Let delivery retry after provider throttling and reduce redundant destinations. | | Messages reach the wrong channel | Replace the webhook with one belonging to the intended channel. | Use delivery history to confirm that a test actually reached the provider. Disabling a connection stops pending delivery; an already-sent request may still be received. # Python SDK (/integrations/python-sdk) The Openstead Python SDK supports Python **3.10 and newer**, with HTTPX transport and Pydantic models. `Openstead` and `AsyncOpenstead` expose the same public core resources. ## Install [#install] Install the Openstead SDK from the private [Python repository](https://github.com/Layerrail/openstead-python). Your GitHub identity needs repository access and an authenticated Git credential helper. Contact [Openstead support](https://openstead.tech/contact) to request access if the repository is unavailable. ```bash python -m pip install "git+https://github.com/Layerrail/openstead-python.git@32e1ccc913b2dd7ad3d964da46bd63f92e2a6de0" ``` For a local checkout, run `python -m pip install .` from the repository directory. Pin a reviewed source commit in production dependency files. The SDK is not published on PyPI; never insert a GitHub token into an installation command or dependency file. The examples below use the Openstead source version. Existing installations can keep using the `runivo` import and `Runivo` / `AsyncRunivo` compatibility aliases after upgrading. `OPENSTEAD_API_KEY` takes precedence, with `RUNIVO_API_KEY` retained as a fallback. ## Read services [#read-services] Set `OPENSTEAD_API_KEY` in your secret environment and `OPENSTEAD_WORKSPACE_ID` to its workspace UUID. ```python import os from openstead import Openstead with Openstead() as client: workspace_id = os.environ["OPENSTEAD_WORKSPACE_ID"] workspace = client.workspaces.get(workspace_id=workspace_id).workspace print(workspace.name) for service in client.services.iter(workspace_id=workspace_id): print(service.name, service.status, service.url) ``` The default API root is `https://api.openstead.tech/api/v1`. Pass `api_key=` explicitly to override the environment. Reuse a client in long-lived applications, and close it with a context manager or `close()`. ## Async usage [#async-usage] ```python import asyncio import os from openstead import AsyncOpenstead async def main() -> None: async with AsyncOpenstead() as client: async for project in client.projects.iter( workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], ): print(project.id, project.name) asyncio.run(main()) ``` Await endpoint methods; use `async for` for iterators. Use `async with` or `aclose()` to close an asynchronous client. ## Create a project [#create-a-project] This creates configuration and requires a write key: ```python import os from openstead import Openstead from openstead.models import ProjectCreate with Openstead() as client: response = client.projects.create( workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], data=ProjectCreate(name="example-project", color="violet"), idempotency_key="create-example-project-001", ) print(response.project.id) ``` `data=` accepts typed models or mappings with API field names. Models expose Python `snake_case` attributes, such as `live_deployment_id`. Serialize wire names with `model_dump(by_alias=True, mode="json")` when needed. ## Resources and helpers [#resources-and-helpers] | Resource | Methods | | ------------- | ----------------------------------------------------------------------------------------------------- | | `catalog` | `get()` | | `workspaces` | `get()` | | `projects` | `list()`, `iter()`, `get()`, `create()`, `update()`, `delete()` | | `services` | `list()`, `iter()`, `get()`, `create()`, `update()`, `delete()`, `archive()`, `restore()`, `action()` | | `variables` | `list()`, `iter()`, `create()`, `update()`, `delete()`, `reveal()` | | `deployments` | `list()`, `iter()`, `get()`, `create()`, `cancel()`, `rollback()`, `wait()` | | `operations` | `get()`, `wait()` | | `logs` | `list()`, `iter()`, `tail()` | Use `client.get_openapi()` to retrieve the public contract. Endpoint responses keep their named envelope; iterator and wait helpers yield or return the contained model. ## Poll an existing deployment [#poll-an-existing-deployment] ```python import os from openstead import DeploymentFailed, PollTimeout, Openstead with Openstead() as client: try: deployment = client.deployments.wait( workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], service_id=os.environ["OPENSTEAD_SERVICE_ID"], deployment_id=os.environ["OPENSTEAD_DEPLOYMENT_ID"], timeout=300.0, poll_interval=2.0, ) print(deployment.status) except DeploymentFailed: print("Review this deployment's logs before deploying again.") except PollTimeout: print("The wait ended. Read the existing deployment for its current state.") ``` Waits default to 600 seconds and a two-second interval. A local cancellation or timeout ends observation, not the remote deployment. `operations.wait()` returns at `complete` and raises `OperationFailed` on failure. ## Retry configuration and errors [#retry-configuration-and-errors] The client defaults to a 20-second request timeout, at most two retries, and a 30-second retry window. Configure `timeout`, `max_retries`, and `max_retry_elapsed` at construction. Supported writes receive a key automatically; provide a durable `idempotency_key` for retries across separate calls. ```python import os from openstead import APIError, Openstead, TransportError try: with Openstead(max_retries=2) as client: client.projects.list(workspace_id=os.environ["OPENSTEAD_WORKSPACE_ID"], limit=100) except APIError as exc: print(exc.status_code, exc.code, exc.request_id) except TransportError: print("The API request could not complete within its retry limits.") ``` Use `APIError.errors` only when you need the server's detailed messages. Do not log whole requests, responses, or secret models. Secret reveal is an explicit audited operation with no automatic retries. See [retry rules](/api/idempotency) and [log reading](/api/logs). # TypeScript SDK (/integrations/typescript-sdk) The Openstead TypeScript SDK supports **Node.js 22 and newer**, ESM and CommonJS. It provides types and helpers for all 28 core operations. ## Install [#install] Build the SDK from the private [TypeScript repository](https://github.com/Layerrail/openstead-typescript) with an authenticated Git credential helper: ```bash git clone https://github.com/Layerrail/openstead-typescript.git cd openstead-typescript git checkout 0b9b2a7664546bcbbf5841f256e6496c61ea54ad npm ci npm run build npm pack ``` Install the resulting tarball in your application: ```bash npm install /absolute/path/to/layerrail-openstead-0.1.0.tgz ``` The installed package is `@layerrail/openstead`. It is not published to the public npm registry. Pin a reviewed source commit when building production artifacts. Contact [Openstead support](https://openstead.tech/contact) if your GitHub identity needs repository access. The examples below use the Openstead source version. Named `Runivo` classes and error aliases remain available in the new package for existing code. `OPENSTEAD_API_KEY` takes precedence, with `RUNIVO_API_KEY` retained as a fallback. Existing installations of the previous package continue to work; update the package dependency when adopting these imports. ## Connect from your server [#connect-from-your-server] ```ts import Openstead from '@layerrail/openstead'; const client = new Openstead(); // Reads OPENSTEAD_API_KEY. const workspaceId = process.env.OPENSTEAD_WORKSPACE_ID!; const { workspace } = await client.workspaces.get({ workspaceId }); console.log(workspace.name); for await (const service of client.services.iterate({ workspaceId })) { console.log(service.name, service.status); } ``` CommonJS can use `const { Openstead } = require('@layerrail/openstead')`. The default API root is `https://api.openstead.tech/api/v1`. Keep this client in server code. Browser use is disabled by default because a bundled bearer credential would expose workspace access. In Next.js, call it from a server-only module or Route Handler and return only the data that the requesting user is authorized to read. ## Create configuration [#create-configuration] Methods accept an argument object and a separate request-options object: ```ts const { project } = await client.projects.create( { workspaceId, body: { name: 'example-project', color: 'violet' }, }, { idempotencyKey: 'create-example-project-001', requestId: 'trace-project-01', }, ); console.log(project.id); ``` `idempotencyKey` identifies a logical write. The options-level `requestId` sets the diagnostic `X-Request-ID` header. They are separate controls. The SDK preserves API JSON envelopes and camelCase fields. ## Resources [#resources] `catalog`, `workspaces`, `projects`, `services`, `variables`, `deployments`, `operations`, and `logs` expose the corresponding API methods. Projects, services, variables, and deployments provide `iterate()` async generators. A plain `list()` returns one exact API envelope. `client.request(operationId, args, options)` calls the same typed core operations by their OpenAPI operation ID. Use the [reference](/api/reference) for fields and constraints. ## Observe an existing deployment [#observe-an-existing-deployment] ```ts import { OpensteadDeploymentError, OpensteadTimeoutError } from '@layerrail/openstead'; try { const deployment = await client.deployments.wait( { workspaceId, serviceId: process.env.OPENSTEAD_SERVICE_ID!, deploymentId: process.env.OPENSTEAD_DEPLOYMENT_ID!, }, { timeoutMs: 300_000, intervalMs: 2_000, maxAttempts: 150 }, ); console.log(deployment.status); } catch (error) { if (error instanceof OpensteadDeploymentError) { console.error('The deployment ended unsuccessfully.'); } else if (error instanceof OpensteadTimeoutError) { console.error('The wait ended; the remote deployment may still be running.'); } else { throw error; } } ``` Wait returns at `live` and raises for `failed`, `cancelled`, or `superseded`. `operations.wait()` returns at `complete`. Local cancellation stops observation; use an explicit deployment cancellation request to stop a remote deployment. ## Timeouts, retries, and cancellation [#timeouts-retries-and-cancellation] ```ts const controlled = new Openstead({ timeoutMs: 30_000, maxRetries: 2, retryDelayMs: 250, maxRetryDelayMs: 10_000, }); const controller = new AbortController(); const response = await controlled.services.list( { workspaceId, query: { limit: 100 } }, { signal: controller.signal, timeoutMs: 10_000 }, ); console.log(response.services.length); ``` The per-call deadline covers network work, response consumption, and retry sleeps. Reads and supported keyed writes retry eligible transport failures, 408, 429, and 5xx responses. `Retry-After` is honored. Set `maxRetries: 0` to disable retries. Supported writes receive an automatic key that stays fixed across the call's retries. Supply your own key for retries across separate calls. Secret reveal is never retried and rejects an idempotency key. ## Logs and errors [#logs-and-errors] `logs.batches()` drains available batches; `logs.tail()` polls for lines. Both use numeric log cursors and accept cancellation and deadline controls. Their default observation deadline is five minutes. ```ts import { OpensteadAPIError, getResponseMetadata } from '@layerrail/openstead'; try { const result = await client.workspaces.get({ workspaceId }); console.log(getResponseMetadata(result)?.requestId); } catch (error) { if (error instanceof OpensteadAPIError) { console.error({ status: error.status, code: error.code, requestId: error.requestId }); } else { throw error; } } ``` Detailed `error.errors` and log lines may contain application data. Choose what to expose deliberately. Successful response metadata is separate from the wire JSON; inspect it before cloning the result. # Webhooks (/integrations/webhooks) Openstead webhooks notify your systems when events occur in a workspace. Configure them as an owner or admin in the dashboard's **Webhooks** page. ## Create a destination [#create-a-destination] 1. Select **Create Webhook**. 2. Enter a name and a public HTTPS endpoint on port 443. 3. Select the subscribed events. 4. Save and securely store the displayed signing secret. 5. Open delivery history and send a test. The receiver must have a valid TLS certificate and resolve to public addresses. Redirects are not followed. Saving an enabled destination subscribes it to future events. ## Event types [#event-types] | Type | Meaning | | ------------------- | -------------------------------------- | | `deploy.succeeded` | A deployment became live. | | `deploy.failed` | A deployment failed. | | `service.unhealthy` | A service health failure was reported. | | `backup.succeeded` | A backup completed successfully. | | `backup.failed` | A backup failed. | | `integration.test` | An explicitly requested delivery test. | ## Payload [#payload] ```json { "id": "00000000-0000-4000-8000-000000000001", "type": "deploy.succeeded", "version": 1, "createdAt": "2026-09-28T12:00:00Z", "workspaceId": "00000000-0000-4000-8000-000000000002", "data": { "serviceId": "00000000-0000-4000-8000-000000000003", "serviceName": "example-api", "title": "Deployment succeeded", "description": "The service deployment is live." } } ``` IDs and descriptions above are illustrative. Treat descriptive text as display content, not a fixed schema for extracting state. Use the event type and resource identifiers for processing. ## Verify the signature [#verify-the-signature] Deliveries include these headers: | Header | Value | | -------------------- | --------------------------------------------- | | `X-Runivo-Delivery` | Delivery UUID; stable across retries. | | `X-Runivo-Event` | Event type. | | `X-Runivo-Timestamp` | Unix timestamp for this attempt. | | `X-Runivo-Signature` | `v1=` followed by the HMAC-SHA256 hex digest. | The signed input is the timestamp, a period, and the **exact request-body bytes**. Do not parse and reserialize JSON before verifying. The following standalone Python function verifies the body and returns its JSON object: ```python import hashlib import hmac import json import time def verify_openstead_webhook(body: bytes, headers, secret: str) -> dict: stamp = headers.get("X-Runivo-Timestamp", "") signature = headers.get("X-Runivo-Signature", "") if not stamp.isascii() or not stamp.isdigit() or len(stamp) > 12: raise ValueError("Invalid webhook timestamp") if abs(time.time() - int(stamp)) > 300: raise ValueError("Webhook timestamp is outside the allowed window") if (len(signature) != 67 or not signature.startswith("v1=") or any(char not in "0123456789abcdef" for char in signature[3:])): raise ValueError("Invalid webhook signature format") expected = "v1=" + hmac.new( secret.encode("utf-8"), stamp.encode("ascii") + b"." + body, hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): raise ValueError("Invalid webhook signature") event = json.loads(body) if not isinstance(event, dict): raise ValueError("Expected a webhook object") if event.get("id") != headers.get("X-Runivo-Delivery"): raise ValueError("Delivery ID does not match the signed event") return event ``` Use a case-insensitive header mapping supplied by your HTTP framework. Load `secret` from a secret store, validate the expected workspace and supported event type, then durably record or enqueue the event. Keep a uniqueness constraint on its delivery ID so repeated deliveries cannot apply the same business action twice. Return a 2xx response after accepting the event durably. Process expensive work asynchronously. A configured integration token is sent separately as an Authorization Bearer header. ## Retries and delivery history [#retries-and-delivery-history] Delivery is **at least once**. A 2xx response succeeds. Other status codes and transport failures retry with exponential backoff, up to eight attempts. The delivery ID and body stay the same across attempts; the timestamp and signature are refreshed. Use delivery history to inspect status, HTTP response, attempt count, and safe error details. Retry failed deliveries after fixing the endpoint. Editing or disabling a connection cancels pending deliveries, although a request already in flight may still arrive. Delivery history is retained for 30 days. Saving a new connection does not replay events from before its activation. # Access controls and maintenance (/networking/access-controls) Web services and static sites have public routing settings for IP restrictions and maintenance responses. These settings apply to their public HTTP and HTTPS hostnames, including the generated Openstead address and attached custom domains. ## Restrict requests by IP address [#restrict-requests-by-ip-address] Open the service's **Settings → Networking → Allowed IP addresses**. Enter one IP address or CIDR range per line, then save. For example, `203.0.113.10/32` represents one IPv4 address and `203.0.113.0/24` represents a range. These are documentation examples: use the actual public egress addresses of your office, VPN, or intended client. An empty list allows public access. A nonempty list permits requests only from matching sources. Confirm your own source address is included before applying a restrictive list, and test from both an allowed and a disallowed connection. The routing layer evaluates the source address it sees. If traffic arrives through another proxy, its outgoing IP ranges may be the visible source. A forwarded header supplied by an arbitrary client is not a reason to trust that client's claimed address. IP restrictions complement application authentication. They do not grant logged-in application access or replace password, token, and permission checks. Domain verification still follows the direct-DNS requirements in [Custom domains](/networking/custom-domains). ## Wait for routing confirmation [#wait-for-routing-confirmation] Saving an IP restriction or maintenance change queues a routing update. You do not need to rebuild the application for these settings. Wait for **Routing settings applied** before assuming a policy is active. If the update cannot be confirmed, inspect the displayed message and use **Retry routing update**. A saved form value alone is not evidence that the proxy applied it. For an undeployed service, routing settings apply when the service first deploys. ## Turn on maintenance mode [#turn-on-maintenance-mode] On an eligible paid instance, open **Settings → Maintenance Mode** and enable the switch. Once routing is applied, visitors receive a maintenance page with HTTP `503`. The application keeps running. Maintenance mode changes public routing; it does not suspend compute, stop workers, pause cron jobs, or prevent private-network access. If you need to stop all writes for a database migration, coordinate every writer separately. Disable maintenance mode and wait for the routing confirmation to restore normal public requests. Test a representative request afterward. ## Supply a custom maintenance page [#supply-a-custom-maintenance-page] Set **Custom Maintenance Page** to a public HTTPS URL that returns `200` with `Content-Type: text/html`. The page must fit within 256 KiB and be accessible without login. Use a self-contained document with inline styles. Scripts are disabled in the served maintenance page. The responder permits inline styling and images from HTTPS or data URLs; do not depend on a frontend bundle, remote stylesheet, or the unavailable application's asset routes. Openstead fetches the page when applying routing and serves it with HTTP `503`, not as a redirect to the external URL. If the custom page cannot be loaded, Openstead uses its default maintenance page and reports the fallback. Keep the page on a location that remains available during maintenance. Do not use a URL that depends on the same unavailable application to deliver its content. ## Troubleshoot access [#troubleshoot-access] If a legitimate request is denied, check its actual source IP, the CIDR syntax, any proxy in the path, and whether the latest routing update succeeded. For a page that remains in maintenance, check the switch and the applied status separately. Private database ports are not exposed by these settings. Use [private networking](/networking/private-networking) and the database's own credentials for internal connections. # Caching and response headers (/networking/caching) Use HTTP response headers to tell browsers and compatible caches which responses they may store. Choose the policy according to the content, rather than applying a long cache lifetime to every route. Openstead serves versioned static releases. This is not a promise of a global CDN, and the saved **Cache profile** selector alone does not configure a response cache. Use explicit headers and verify the responses your visitors receive. ## Set static-site headers [#set-static-site-headers] Open the static site's **Settings → Custom HTTP headers**, add a path pattern, header name, and value, then deploy to publish the change. For a site whose `/assets/` directory contains only content-hashed build assets: | Path | Header | Value | | ------------- | --------------- | ------------------------------------- | | `/assets/*` | `Cache-Control` | `public, max-age=31536000, immutable` | | `/index.html` | `Cache-Control` | `no-cache` | Use the real asset paths generated by your framework. The long asset lifetime is appropriate only when every content change produces a new URL. A file called `app.js` that changes without changing its URL should not receive a year-long immutable policy. `no-cache` allows storage but requires revalidation before reuse. `no-store` instructs clients not to store the response. They have different meanings. ## Set dynamic response headers in the application [#set-dynamic-response-headers-in-the-application] For web services, configure response headers in your framework or application server. Responses containing account information, personalised HTML, or private API data should not use a broad public cache policy. For example, an Express handler for a private account response can set: ```js response.set('Cache-Control', 'private, no-store'); response.json(accountSummary); ``` This example assumes the request has already been authenticated and authorised. Caching headers do not provide access control. ## Publish updates safely [#publish-updates-safely] Use content-hashed asset filenames so a deployment can reference new assets immediately while older browser caches remain valid. Keep HTML lifetimes shorter than immutable assets so visitors discover the new references. Changing an Openstead variable or response-header setting does not rewrite already cached files on every visitor's device. Deploy the updated site, confirm its served headers, and use new asset URLs when replacing immutable content. A code rollback changes the active release. It does not erase copies already stored by browsers or an external cache. ## Verify the actual response [#verify-the-actual-response] Inspect a representative page and asset with your browser's network panel or: ```bash curl --head https://app.example.com/ curl --head https://app.example.com/assets/app-CONTENT_HASH.js ``` Replace these example URLs with paths that exist in your release. Check `Cache-Control`, content type, status, and any `Vary` header used by the application. Test authenticated and unauthenticated requests separately. An asset URL returning HTML often indicates a missing file or an overly broad SPA rewrite, not a cache problem. See [Static sites](/services/static-sites). ## External cache providers [#external-cache-providers] If you add an external caching service, follow its own invalidation and authentication requirements. Do not enable Cloudflare proxying merely to try caching while using Openstead's current direct-DNS domain-verification flow; see [Cloudflare DNS](/networking/cloudflare). Openstead's maintenance and platform-error responses use `no-store`. Ensure your own error and personalised responses have a deliberate policy as well. # Cloudflare DNS (/networking/cloudflare) You can keep Cloudflare as your DNS provider while hosting the application on Openstead. Openstead's domain verification requires the hostname to resolve directly to the service's displayed IPv4 destination. ## Before changing records [#before-changing-records] Deploy the service, add the hostname under **Custom Domains**, and keep the DNS-record instructions open. Record the current DNS settings so you can review an existing site's cutover. Do not change mail-related MX, SPF, DKIM, or DMARC records when connecting only a website hostname. ## Add a subdomain [#add-a-subdomain] For `app.example.com`, create these records in Cloudflare's DNS page: | Type | Name | Content | Proxy | | ----- | ----------------------- | --------------------------------------- | -------------- | | TXT | `_runivo-challenge.app` | Exact verification value from Openstead | Not applicable | | CNAME | `app` | Service target from Openstead | **DNS only** | Cloudflare shows DNS-only mode as the grey cloud. Do not put `https://` or a path in the CNAME target. ## Add the root domain [#add-the-root-domain] For `example.com`, use the service's root-domain A-record alternative: | Type | Name | Content | Proxy | | ---- | ------------------- | --------------------------------------- | -------------- | | TXT | `_runivo-challenge` | Exact verification value from Openstead | Not applicable | | A | `@` | IPv4 address displayed in Openstead | **DNS only** | If you also want `www.example.com`, add it as another custom domain in Openstead and publish its own records. One hostname's verification does not automatically verify the other. ## Resolve conflicts [#resolve-conflicts] Remove conflicting A records, CNAMEs, or AAAA records for the hostname being moved. A stale AAAA record can send IPv6-capable visitors elsewhere even while the A record looks correct. Do not remove unrelated records or all records for the domain. Limit changes to the exact hostname and its Openstead ownership TXT record. ## Keep DNS-only mode [#keep-dns-only-mode] An orange-cloud proxied record returns Cloudflare addresses rather than the service's direct destination. Openstead's verification will report that the hostname does not point to the expected service. Keep the record DNS-only for this integration, including after the initial verification, because Openstead rechecks routing and certificate status. Your HTTPS connection is served with the certificate managed by Openstead. Cloudflare proxy features do not apply to a DNS-only hostname. ## Verify [#verify] Return to Openstead and let automatic verification continue. The page reports DNS verification and HTTPS status separately. If needed, select **Verify DNS** once after correcting records. With DNS tools installed, you can inspect public answers: ```bash nslookup -type=TXT _runivo-challenge.app.example.com nslookup app.example.com ``` Replace the example with your hostname. Local resolver caches can show older results temporarily. The dashboard's detailed message indicates whether ownership, routing, or the certificate is still pending. # Custom domains (/networking/custom-domains) Add a custom domain to serve your application at an address such as `app.example.com` or `example.com`. Custom domains are available for web services and static sites. You need access to the domain's DNS provider and a deployed service. Domain registration is separate from hosting on Openstead. ## Add a hostname [#add-a-hostname] 1. Open your service's **Settings → Custom Domains** section. 2. Select **Add Custom Domain**. 3. Enter the hostname without `https://`, a port, or a path. 4. Add the DNS records shown by Openstead at your DNS provider. Add every hostname separately. `example.com` and `www.example.com` are different hostnames. Openstead supports up to 20 custom hostnames per service; wildcard hostnames are not supported by this flow. ## Publish the ownership record [#publish-the-ownership-record] Openstead displays a TXT record with a name such as `_runivo-challenge.app.example.com` and a generated value. Copy its exact value from the dashboard. The TXT record proves that you control the hostname. Keep it in DNS. If your provider automatically appends `example.com` to record names, enter only the appropriate relative label, such as `_runivo-challenge.app`. ## Point traffic to Openstead [#point-traffic-to-openstead] For a subdomain, add the supplied CNAME record: | Type | Name | Target | | ----- | ----- | ------------------------------------- | | CNAME | `app` | The service target shown in Openstead | For a root domain, use the displayed **Root domain alternative** A record: | Type | Name | Target | | ---- | ---- | --------------------------------------- | | A | `@` | The IPv4 address shown for this service | Use your actual dashboard values, not another service's hostname or IP. Remove conflicting A or AAAA records for the same hostname. There must not be an IPv6 route pointing somewhere else. If your DNS provider proxies traffic, use **DNS-only** mode. See [Cloudflare DNS](/networking/cloudflare) for its exact record setup. ## Wait for automatic verification [#wait-for-automatic-verification] Openstead begins checking when you add the domain and retries automatically. The modal and domain list update as DNS and HTTPS checks finish. You can close the modal; the background verification continues. **Verify DNS** requests another check, but repeatedly pressing it cannot make your DNS provider propagate records faster. Domain verification and certificate issuance are separate steps: 1. Openstead finds the expected ownership TXT record. 2. The hostname resolves to the service's expected IPv4 destination without conflicting records. 3. Openstead configures the route and obtains an HTTPS certificate. 4. The domain is ready when verification and HTTPS are both complete. ## Configure your application [#configure-your-application] After connecting the domain, update application settings that depend on its public URL. Examples include Laravel's `APP_URL`, Django's allowed hosts and CSRF origins, OAuth callback URLs, CORS allowlists, and transactional-email links. DNS does not automatically configure redirects between an apex domain and `www`. Add the needed hostnames and configure any canonical-host redirect in the application or an appropriate routing rule. ## Keep or disable the Openstead address [#keep-or-disable-the-openstead-address] The generated Openstead address can remain enabled beside the custom domain. You can disable it after at least one custom domain has verified HTTPS. If you later remove the last verified custom domain, first re-enable the Openstead subdomain so the service retains a usable public address. ## Troubleshoot a pending domain [#troubleshoot-a-pending-domain] * Check the exact TXT name and value, including whether the DNS provider duplicated the zone name. * Confirm the service is deployed and its target is current. * Remove stale A or AAAA records and disable proxying. * Check both `www` and the root separately if both should work. * If DNS is verified but HTTPS is pending, follow [HTTPS troubleshooting](/networking/https). Deleting a domain in Openstead removes its route. It does not edit or delete records at your DNS provider. # HTTPS and certificates (/networking/https) Openstead manages HTTPS certificates for verified custom domains on web services and static sites. You do not need to upload a private key to connect a standard hostname. ## Certificate activation [#certificate-activation] First publish the ownership and routing records shown in [Custom domains](/networking/custom-domains). Openstead verifies DNS, configures the hostname's route, and checks that a valid certificate is served for that hostname. A domain can be verified while its certificate is still pending. Wait until the dashboard reports that HTTPS is active before using the hostname for production callbacks, login links, or customer communications. Certificates renew automatically while the domain remains correctly routed and the verification requirements continue to be met. Keep the ownership TXT record and current DNS routing. ## HTTPS at the application [#https-at-the-application] The public HTTPS connection terminates at Openstead's routing layer. Your application should listen on its configured internal port using the framework's normal production server settings. Configure the framework's trusted-proxy behaviour appropriately so it generates HTTPS links and secure cookies. A mismatch between the application's expected scheme and proxy headers can cause redirect loops or incorrect callback URLs. Do not disable certificate validation in an external client to hide a hostname or certificate error. Correct the domain setup instead. ## Troubleshoot certificate issuance [#troubleshoot-certificate-issuance] | Symptom | What to inspect | | ------------------------------ | -------------------------------------------------------------------------------------- | | DNS still pending | Ownership TXT record, destination A/CNAME, stale AAAA records, and proxying | | Domain verified, HTTPS pending | Allow certificate activation time; check DNS remains direct and stable | | Certificate for another host | Confirm the exact hostname was added and points to this service | | Browser redirects repeatedly | Application URL, trusted proxy settings, canonical-host redirect, and HTTPS middleware | | Mixed-content warning | Application assets or API URLs hardcoded with `http://` | If the domain has restrictive CAA records, review whether they permit the certificate authority being used for issuance. Contact [support](https://openstead.tech/contact) with the domain and the dashboard's error detail before weakening an existing DNS policy indiscriminately. ## Database transport is separate [#database-transport-is-separate] Website HTTPS does not turn a private database port into a public TLS endpoint. PostgreSQL, MySQL, and Key Value use their [private connection model](/databases/connections). Do not assume a website certificate applies to a database client's connection string. ## Requesting help [#requesting-help] Include the hostname, time of the last DNS change, DNS provider, displayed domain status, and any browser error. A screenshot of DNS records can help after sensitive unrelated records are removed. Never send certificate private keys or account credentials. # Outbound IP addresses (/networking/outbound-ips) Some external databases and APIs allow traffic only from known public addresses. Openstead's dedicated outbound IP connection identifies the static IPv4 address used by your workspace runtime. This is the source address of outbound requests. It is different from a custom domain that routes visitors into an application. ## Configure an outbound IP connection [#configure-an-outbound-ip-connection] 1. Make sure the workspace has an active qualifying paid service. 2. Open the workspace's **Connections → Dedicated IPs** area. 3. Create the connection and describe the service you need to reach. 4. Wait for its status to become active. 5. Copy the verified address from the connection details into the external provider's allowlist. The workspace runtime must be deployed before its outbound routing can be verified. A pending connection is not evidence that an address is ready to use. ## Scope [#scope] The address is associated with the workspace runtime and shared by services in that workspace. It is not a unique address per application or per deployment. Allowlist only the required destination ports and still use the external service's authentication. An IP allowlist is an additional access control, not a replacement for passwords, API keys, or TLS. ## Operational changes [#operational-changes] Use the currently active connection details as the authority. Review external allowlists after a networking change or workspace-runtime recovery. Do not hardcode an address copied from another workspace or from a documentation example. If verification fails, check the connection's displayed message and contact [support](https://openstead.tech/contact) with its identifier and intended destination. Do not expose third-party access credentials in the description. ## Troubleshooting an external connection [#troubleshooting-an-external-connection] Confirm the external provider saved the IPv4 address, permits the destination port, and accepts credentials from this application. Run a connection attempt from the deployed application; a successful request from your laptop has a different source address and does not test the Openstead allowlist. # Private Links (/networking/private-links) A Private Link connects your workspace to an external Azure Private Link service over a private endpoint. Use it when the external service publishes an appropriate Azure Private Link resource or alias and its owner can approve the connection. This is not a general VPN or automatic peering with every cloud database. ## Requirements [#requirements] * An active qualifying paid service in the workspace. * The target's Azure Private Link service resource ID or alias. * The TCP port the target service accepts. * Approval from the external service owner. * Application-level credentials required by that external service. The connection applies to services in the workspace. Confirm this scope is appropriate before adding a target that holds sensitive data. ## Create the connection [#create-the-connection] Open **Connections → Private Links**, create a link, and supply its name, description, target resource ID or alias, and TCP port. Save the connection and review its status. Ask the target owner to review and approve the corresponding endpoint request. Saving a connection in Openstead does not automatically grant permission on the external service. Wait for an active status and use the verified connection details displayed in the dashboard. A pending or failed status should not be treated as a working private endpoint. ## Configure the application [#configure-the-application] Store the target's authentication credentials in the application's environment variables or secret files. Follow the external provider's client, hostname, TLS, and certificate requirements. Only the configured allowed port should be used. A successful connection to one target port does not imply general access to the external network. ## Update or remove a link [#update-or-remove-a-link] Review current consumers before changing a target or port. Applications using the link can lose connectivity during a change. Removing a connection removes its managed private-endpoint access; it does not delete the external provider's application or database. ## Troubleshooting [#troubleshooting] Check the target identifier, owner's approval, configured port, and the external service's availability. Then inspect application credentials and DNS requirements. If approval remains pending, the external owner is the right party to confirm it. For help, send support the connection ID and a redacted description of the target and failure. Do not include authentication secrets or private dataset contents. # Private networking (/networking/private-networking) Openstead services can communicate over the private network associated with their environment. Use private addresses for database traffic and internal application calls. ## Environment boundaries [#environment-boundaries] Place related services in the same project environment: for example, a web service, worker, and PostgreSQL database in Production. Put staging resources in a separate Staging environment. The private hostname shown on a service's **Connections** page is intended for applications in the same environment. A browser or laptop cannot reach it as a public URL. Moving one side of a connection to another environment can break its reachability. Do not reuse a production database reference for an isolated preview. Give the preview its own appropriate data source and credentials. See [Preview environments](/deployments/previews). ## Connect an internal application [#connect-an-internal-application] Create a [private service](/services/private-services), configure its listening port, and start it. Copy its private hostname from Connections into the calling application's configuration. For an internal HTTP service called `internal-api` listening on port 8000, an application variable might be: ```text INTERNAL_API_URL=http://internal-api:8000 ``` Use the actual hostname displayed by your service. The application must listen on `0.0.0.0` to accept connections from other services, rather than binding only to `127.0.0.1`. Private reachability does not replace application authentication. Authenticate sensitive internal endpoints and validate requests as appropriate for your system. ## Connect a managed database [#connect-a-managed-database] Use [connection references](/databases/connections) for PostgreSQL, MySQL, or Key Value. They keep source selection and variable mapping in Openstead and apply the credentials on the next application deploy. The database must be running before a dependent deployment can resolve its reference. If it is recovering, wait for the import to finish. If a paid database was paused after expiry, renew it before reconnecting the application. ## Public and private entry points [#public-and-private-entry-points] | Service | Internet-facing endpoint | Typical private use | | --------------- | ----------------------------------------- | ----------------------------------- | | Web service | Openstead URL and optional custom domains | Application endpoints | | Static site | Openstead URL and optional custom domains | Not a database or internal process | | Private service | No public application domain | Internal HTTP/TCP service | | Database | No public database port | Authenticated database connections | | Worker or cron | No incoming web endpoint | Outbound calls to internal services | MySQL's secure phpMyAdmin interface is a browser gateway to a private database, not a published MySQL port. ## External network access [#external-network-access] For a partner that allowlists your application's public source address, see [Outbound IPs](/networking/outbound-ips). For a supported external Azure Private Link service, see [Private Links](/networking/private-links). These are different from connecting two Openstead services inside one environment. A third-party private service requires its own target configuration and permission. ## Troubleshooting [#troubleshooting] Check the exact environment, current private hostname, and configured port. Then check whether the destination is running and actually listening on all interfaces. A successful public homepage response does not prove an internal TCP port is configured correctly. Use an authorised application shell for network checks where available. Do not expose a database to the internet merely to debug an internal connection. # Background workers (/services/background-workers) A background worker runs a continuous process without a public URL. It can consume queued emails, generate reports, process media, or perform other work that should not keep a user's HTTP request open. ## Create a worker [#create-a-worker] 1. Create a **Background Worker** in your project. 2. Select its source. A web service and worker can use the same repository with different start commands. 3. Configure the build, then set the command that runs the worker in the foreground. 4. Add database, queue, and application variables. Use private connections for Openstead databases and Key Value. 5. Select a paid plan and deploy. Example start commands: | Application | Example | | ----------- | --------------------------------------------------------- | | Celery | `celery -A config worker --loglevel=info` | | Laravel | `php artisan queue:work --sleep=3 --tries=3 --timeout=90` | | Node.js | `node dist/worker.js` | | Sidekiq | `bundle exec sidekiq` | Replace module names and paths with the ones in your application. A worker should keep running; do not daemonize it or send the main process into the background. ## Make jobs safe to repeat [#make-jobs-safe-to-repeat] A process can stop after it has changed data but before it acknowledges a queue message. Make each job idempotent: store a job identifier, check whether the work has already completed, and use database transactions where appropriate. Configure retries, retry delays, time limits, and dead-letter handling in your queue library. Openstead running the worker does not configure those application-level policies for you. ## Connect a queue [#connect-a-queue] For a Redis-compatible queue, create [Key Value](/databases/key-value) and supply its private connection URL to both the producer and worker. Select an eviction policy appropriate for the queue; evicting an unprocessed queue entry can lose work. For Laravel's database queue, the worker and web service need access to the same intended database. Run the queue schema migrations before accepting jobs. ## Deploy without losing track of work [#deploy-without-losing-track-of-work] Handle termination gracefully and give unfinished messages back to the queue. Release long-running database connections when exiting. Inspect **Logs** after a deployment to confirm the worker actually connects and consumes work; a running process alone does not prove that jobs are being completed. Stateless workers can use [scaling](/deployments/scaling). Confirm that adding consumers is safe for the queue and database connection pool. Use a [cron job](/services/cron-jobs) for a command that should start on a schedule, complete, and exit. # Cron jobs (/services/cron-jobs) A cron job starts a fresh execution when its schedule is due. Use it for periodic reports, cleanup, synchronization, or commands such as Laravel's `schedule:run`. The command should finish and exit; continuous queue consumers belong in a [background worker](/services/background-workers). ## Create a cron job [#create-a-cron-job] 1. Create a **Cron Job** and select a repository or image. 2. Configure its build and the command to execute. Repository-based jobs require an explicit start command. 3. Choose a paid instance plan, then add variables and secret files. 4. Enter a five-field cron expression, an IANA time zone, and a timeout. 5. Review the upcoming occurrences shown in the dashboard and build the job. The first successful build provides the image and environment used for subsequent executions. Cron jobs do not keep a permanent application instance listening between runs. ## Write a schedule [#write-a-schedule] The five fields are minute, hour, day of month, month, and day of week. | Schedule | Meaning | | -------------- | --------------------------------------- | | `*/15 * * * *` | Every 15 minutes | | `0 * * * *` | At the start of each hour | | `0 9 * * 1-5` | At 09:00 on weekdays | | `0 2 1 * *` | At 02:00 on the first day of each month | Choose `UTC` for a schedule independent of local daylight-saving changes. Choose a zone such as `Africa/Lagos` when the schedule should follow that zone's local time. Always inspect the displayed next runs, especially around clock changes. ## Understand executions [#understand-executions] Only one execution runs at a time for a given cron service. If schedules are missed while work is delayed, Openstead coalesces them instead of replaying every missed interval. At most one scheduled execution waits behind an active run. **Manually triggering a cron job replaces its active or pending execution.** Check the run history before using the manual trigger on work that must finish uninterrupted. Each execution uses the last successful build's command, variables, and secret files, and has access to the project's permitted private network. Editing the schedule or timeout takes effect without rebuilding. Deploy a new build to apply command or environment changes. ## Timeouts and results [#timeouts-and-results] The cron timeout can be between one second and 12 hours. A successful command exits with code `0`; nonzero exit codes indicate failure. Use the run history to inspect output, start and finish times, and the exit code. Design scheduled work to tolerate retries and partial completion. Scheduling is not an exactly-once guarantee for external side effects such as charging a customer or sending an email. Use application-level deduplication. Do not rely on files left by an earlier execution. Store durable state in a database or object storage. Suspending the service prevents new scheduled work until it is resumed. # Choose a service (/services/overview) A service is a separately configured and deployed part of your application. A project groups related services, such as a frontend, API, worker, and database. Openstead provides the infrastructure; you connect your code and choose how it runs. ## Compare service types [#compare-service-types] | Service | Use it for | How it runs | Public URL | | ------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------ | ------------------- | | [Web service](/services/web-services) | APIs, server-rendered websites, application backends | A continuously running application process | Yes | | [Static site](/services/static-sites) | HTML, CSS, JavaScript, client-rendered frontends | Files produced by a build | Yes | | [Private service](/services/private-services) | Internal HTTP services and application components | A running process reachable through private networking | No | | [Background worker](/services/background-workers) | Queue consumers and asynchronous processing | A continuously running worker process | No | | [Cron job](/services/cron-jobs) | Periodic reports, cleanup, scheduled application commands | A fresh execution on a schedule | No | | [PostgreSQL](/databases/postgresql) | Relational application data | A managed database with persistent storage | Private connections | | [MySQL](/databases/mysql) | MySQL applications, including Laravel | A managed database with persistent storage | Private connections | | [Key Value](/databases/key-value) | Redis-compatible caches, queues, shared state | A managed Redis-compatible service | Private connections | ## Choose between a web service and a static site [#choose-between-a-web-service-and-a-static-site] Choose a **web service** when requests execute server code. Next.js server rendering, Django views, Laravel routes, and Express APIs need a running server. Choose a **static site** when your build produces a directory that can be served as files. Vite frontends and exported Next.js sites are common examples. A static site cannot execute PHP or open a private database connection from the browser. A frontend and API can be separate services. Configure the frontend with the API's public URL and configure the API's CORS policy for the frontend's origin. ## Build an application from several services [#build-an-application-from-several-services] For a typical application: 1. Create a [project](/account/projects). 2. Add a database in the intended environment. 3. Deploy the API as a web service and connect it to that database privately. 4. Add a worker if the application processes jobs outside HTTP requests. 5. Add a static frontend or serve the frontend from the web service. 6. Attach [custom domains](/networking/custom-domains) to the public services. Deploying one service does not automatically deploy the others. Review each service's source, variables, plan, and deployment status. Use a [Blueprint](/blueprints/overview) when you want to describe related resources together. ## Keep application data outside the container filesystem [#keep-application-data-outside-the-container-filesystem] Code and dependencies belong in the deployment artifact. Store relational data in a database, and store uploaded files in object storage or a [persistent disk](/databases/persistent-disks). Files written only to a running container can disappear when it is replaced. For your first application, follow [Deploy your first service](/getting-started/first-deploy). # Private services (/services/private-services) A private service runs an application that other permitted Openstead services can reach through private networking. Use it for an internal API or a backend component that should not have a public HTTPS endpoint. ## Create a private service [#create-a-private-service] 1. Create a **Private Service** in the same project and network scope as its clients. 2. Select the repository or container image and configure its build and start commands. 3. Choose a paid instance plan and add required variables. 4. Confirm that the application's listening port matches the service configuration. Use [Update service](/api/reference/services/updateService) to set `configuration.port` and an optional `configuration.healthCheckPath` for an internal HTTP application. 5. Deploy, then copy the private hostname from the service's connection details. The server must bind to `0.0.0.0`, not only `127.0.0.1`. For an internal HTTP API listening on port `8000`, its clients use a URL shaped like `http://:8000`. ## Connect from another service [#connect-from-another-service] Store the private URL in the calling service's environment, such as `INTERNAL_API_URL`. The actual hostname must come from Openstead's connection details. A browser on a customer's laptop cannot resolve or connect to this private address. Read [Private networking](/networking/private-networking) before connecting across projects or isolated environments. Placing resources in the same workspace does not mean every isolation boundary is removed. ## Keep authentication where it matters [#keep-authentication-where-it-matters] Private reachability controls network access. Your application should still authenticate requests that can read sensitive records or change state. Use narrowly scoped credentials between services and apply request timeouts so a slow dependency does not exhaust the caller's connections. ## Deploy and scale [#deploy-and-scale] Private services use the same build lifecycle, logs, and paid compute options as web services. Stateless instances can use [manual or automatic scaling](/deployments/scaling). A service with a persistent disk is restricted to one instance. Choose a [background worker](/services/background-workers) instead when the process consumes a queue and does not need to accept network requests. Choose a [web service](/services/web-services) when the application needs a public URL or a custom domain. # Static sites (/services/static-sites) A static site serves files produced by your build. Use it for a Vite frontend, a Next.js static export, a documentation site, or plain HTML. Server-side code, database drivers, and background processes belong in a web service or worker. ## Deploy a static site [#deploy-a-static-site] 1. Create a **Static Site** in your project. 2. Connect the repository and select its branch and root directory. 3. Confirm the build command and **Publish directory**. 4. Add any public build-time configuration, then deploy. 5. Open the service URL and test both the homepage and a nested route. | Project | Typical build command | Publish directory | | ------------------------------- | --------------------------------------- | ----------------------------------------- | | Vite | `npm run build` | `dist` | | Next.js with `output: 'export'` | `npm run build` | `out` | | Astro with static output | `npm run build` | `dist` | | Create React App | `npm run build` | `build` | | Plain HTML | Leave empty if no compilation is needed | `.` or the directory containing your site | Use the package manager recorded by your project. The publish directory is relative to the selected application root for Railpack builds. For Dockerfile builds, it is extracted relative to the final image's working directory. ## Handle client-side routes [#handle-client-side-routes] A frontend router can make `/account` work after clicking a link while a direct visit still returns 404. If your build uses client-side routing, add a rule under **Redirects & rewrites**: | Setting | Value | | ----------- | ------------- | | Source path | `/*` | | Destination | `/index.html` | | Action | Rewrite (200) | Deploy again to publish the rule. Confirm that JavaScript, CSS, image, and favicon URLs still return the correct files. Do not add a blanket SPA fallback to a site that needs real page-level 404 responses or has no client router. ## Configure response headers and domains [#configure-response-headers-and-domains] Use **Custom HTTP headers** for path-specific response headers, then deploy those changes. Be cautious with caching HTML: hashed JavaScript and CSS assets can be cached longer than the HTML that references them. See [HTTP caching](/networking/caching) for response-header examples and the difference between a saved preference and an effective caching policy. Add your own hostname through [Custom domains](/networking/custom-domains). Both the Openstead URL and verified custom domains use HTTPS. ## Understand configuration and releases [#understand-configuration-and-releases] Static configuration is compiled into files. Changing a variable in Openstead requires a new deployment to change the served site. Any variable exposed through mechanisms such as `VITE_*` or `NEXT_PUBLIC_*` must be safe for visitors to read. Openstead publishes versioned static artifacts and retains successful releases for rollback. Static delivery does not require a dedicated application container, so there is no application shell or per-container CPU and memory chart. Openstead's static delivery should not be treated as a promise of a globally distributed CDN. For a guided example, see [Deploy a Vite frontend](/guides/vite). # Web services (/services/web-services) A web service runs your application and exposes it through a public HTTPS URL. Use it for Django, Laravel, Express, FastAPI, Next.js server rendering, and other applications that execute code for incoming requests. ## Create a web service [#create-a-web-service] 1. Open your project in the [Openstead dashboard](https://openstead-dashboard.vercel.app/dashboard) and create a **Web Service**. 2. Choose a GitHub repository or a container image. For a repository, select the branch and application root. 3. Review the detected language, build method, build command, and start command. 4. Select an instance plan. Add environment variables and any required persistent disk. 5. Configure the application's listening port and, preferably, a health check path. 6. Deploy. If a paid month requires activation, complete the displayed checkout first. Follow the deployment's logs until it is **Live**, then open the URL shown on the service. ## Bind to the configured port [#bind-to-the-configured-port] Your server must listen on `0.0.0.0` and the port configured for the service. Openstead supplies that value as the runtime `PORT` variable. Detection commonly selects port `3000` for Node.js; the general configuration default is `8000`. Check the actual service settings rather than assuming a fixed port. For Express: ```js const port = Number(process.env.PORT || 3000); app.listen(port, "0.0.0.0"); ``` For an ASGI application: ```bash uvicorn main:app --host 0.0.0.0 --port "$PORT" ``` Binding only to `localhost` prevents the platform from reaching the application. The application normally serves HTTP inside the service; Openstead handles public HTTPS. ## Configure a health check [#configure-a-health-check] Add a lightweight route such as `/health` that returns a successful response when the application is ready. Set **Health check path** to that route. Without a path, readiness checks verify that the configured port accepts connections. Health checks must work without a browser login. See [Health checks](/deployments/health-checks) for accepted responses and common failures. ## Connect databases and store uploads [#connect-databases-and-store-uploads] Use the database's private connection details from the same permitted network scope. Keep credentials in [environment variables](/deployments/environment-variables), never in client-side JavaScript. Persist user uploads with object storage or a [paid persistent disk](/databases/persistent-disks). A service with a local persistent disk runs one instance. Applications that need multiple instances should use shared external storage, sessions, and queues. ## Operate the service [#operate-the-service] The service navigation includes deployments, logs, metrics, environment variables, and compute settings. Paid running application instances also support shell access and one-off jobs. Use [rollbacks](/deployments/rollbacks) to return to an earlier successful release; a rollback does not reverse database writes. Use [access controls](/networking/access-controls) to restrict incoming IP addresses or configure maintenance mode. Confirm the routing change's result before assuming the new policy is serving. Free web instances sleep after inactivity and share a monthly runtime allowance. Read [Free instances](/getting-started/free-instances) before using a free instance for a workload that must stay awake. Next: [Next.js](/guides/nextjs), [Django](/guides/django), [Laravel](/guides/laravel), or [Node.js](/guides/nodejs).