Projects
Create and configure projects, source settings, and environments.
A project combines source code, build configuration, environment variables, domains, and deployment history. Create a project record first, then submit a deployment.
Local filesystem and edge administration operations require self-hosted capabilities. For Cloud, connect to the Cloud API directly. Fixed scopes do not forward through an owner's Cloud link.
Create a project
Create a project from a git repo or a local path, baking the build config into the project row. For an
uploaded folder, use the source upload flow instead. Only name is required —
everything else is auto-detected or defaulted.
const result = await ship.projects.create({
name: "marketing-site",
gitOwner: "acme",
gitRepo: "site",
gitBranch: "main",
framework: "nextjs",
});
console.log(result);curl -X POST "$OPENSHIP_URL/api/projects" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"marketing-site","gitOwner":"acme","gitRepo":"site","gitBranch":"main","framework":"nextjs"}'Prop
Type
Creating a project does not deploy it
POST /api/projects only records the project and its config. Trigger the first build with the
Deployments API (POST /api/deployments) or openship deploy.
Scale a stateless application on a cluster
Self-hosted projects can target a ready server cluster. Saving the target requires fleet administration. Choose a registry repository for source builds; cluster nodes pull immutable release images using the saved registry credentials. This supports a single application or worker whose instances can start on any selected server. Project-owned shared volumes can persist files across instances; node-local bind mounts and Compose conversion require a separate migration. PostgreSQL and Redis use their own managed database lifecycle and replication.
const current = await ship.projects.getClusterWorkload("proj_123");
await ship.projects.setClusterTarget("proj_123", {
clusterId: "cluster_123",
config: { replicas: 3, imageRepository: "ghcr.io/acme/api" },
expectedUpdatedAt: current.updatedAt,
stateless: true,
});
// Deploy the project through the existing Deployments API.
// After that release is active, change its replicas without rebuilding:
const live = await ship.projects.getClusterWorkload("proj_123");
if (live.activeDeploymentId && live.activeClusterId === live.clusterId) {
const scale = await ship.projects.scaleClusterWorkload("proj_123", {
replicas: 4,
expectedDeploymentId: live.activeDeploymentId,
expectedUpdatedAt: live.updatedAt,
});
console.log(scale.deploymentId); // Follow the normal deployment progress stream.
}Replica changes are configuration releases. They reuse the active image, wait for ready pods and
use the existing update/rollback history. Allow capacity for the old and new releases together.
status and observedAt describe the last observation; saved config describes future intent.
OpenShip Edge currently enters through the first cluster control server. Automatic gateway
failover and policy-driven autoscaling are not implemented.
Share application files
Enable shared storage on the server cluster first. Use createClusterVolume to create a project
volume, then add its name and mount path to the cluster configuration and deploy. Each release
retains its reviewed volume identities. listClusterVolumes and streamClusterVolumeEvents
report attachment health, independent copies, expansion and backups.
Shared-file backups use the cluster's configured external destination. Schedules can run hourly
or daily. listClusterVolumeBackups includes archives whose original volume was deleted;
restore into a new volume and verify its contents before attaching it to the application.
Disconnect and deploy before deleting a volume. External backups survive volume removal.
Add, import or recover a database
Create PostgreSQL or Redis through createClusterDatabase, then inspect progress until ready.
Database instances and Redis shards scale independently of application replicas. A Redis shard
change requires a verified backup and explicit review.
listClusterDatabaseImports returns eligible backups of this project's existing PostgreSQL or
Redis services. Use a selected backup with importFrom, or recover a managed database archive
with restoreFrom. A PostgreSQL 17-to-18 upgrade uses copyFrom to build a separate database.
Each path preserves the source. You can prepare imported databases before changing the app's
cluster target; it must later select the same cluster.
Verify the recovered data, then explicitly save or replace the managed application connection with both databases' current sequences. Redeploy to use it. For a final cutover, pause source writes and take a fresh recovery point; writes after capture are not copied automatically. See the database workflow.
Update a project
The update body is a partial of the create body — send only the fields you want to change. Every field above is accepted and optional here.
const result = await ship.projects.update("proj_123", {
buildCommand: "pnpm build",
gitBranch: "release",
});
console.log(result);curl -X PATCH "$OPENSHIP_URL/api/projects/proj_123" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"buildCommand":"pnpm build","gitBranch":"release"}'Link a git repo
Attach (or re-point) a GitHub repository to an existing project. When branch is omitted, the repo's
default branch is used.
const result = await ship.projects.linkRepo("proj_123", {
owner: "acme",
repo: "site",
branch: "main",
});
console.log(result);curl -X POST "$OPENSHIP_URL/api/projects/proj_123/git/link" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owner":"acme","repo":"site","branch":"main"}'Prop
Type
Related git routes: POST /api/projects/:id/branch (change the deploy branch),
POST /api/projects/:id/auto-deploy (toggle deploy-on-push), GET /api/projects/:id/branches, and
GET /api/projects/:id/commit-status (is the deployed commit behind remote HEAD?). See the
GitHub API for connecting the GitHub App itself.
Connect a custom domain
A shortcut that adds a domain to the project and marks it primary in one call. For the full domain lifecycle (DNS preview, verification, SSL), use the Domains API.
const result = await ship.projects.connectDomain("proj_123", {
domain: "app.example.com",
});
console.log(result);curl -X POST "$OPENSHIP_URL/api/projects/proj_123/connect" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain":"app.example.com"}'Prop
Type
Environment variables
GET /api/projects/:id/env
PATCH /api/projects/:id/envGET lists variables for the project with secret values masked. Writes go through a merge: only the
keys you name are touched — upserts are inserted/updated, deletes are removed, and every other
variable (including untouched masked secrets) is left intact.
const result = await ship.projects.mergeEnvVars("proj_123", {
environment: "production",
upserts: [
{
key: "DATABASE_URL",
value: "postgres://…",
isSecret: true,
},
],
deletes: ["OLD_FLAG"],
});
console.log(result);curl -X PATCH "$OPENSHIP_URL/api/projects/proj_123/env" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"environment": "production",
"upserts": [{"key":"DATABASE_URL","value":"postgres://…","isSecret":true}],
"deletes": ["OLD_FLAG"]
}'Prop
Type
There is no full-replace endpoint
The old destructive PUT /:id/env was removed — it could wipe or corrupt masked secrets. Always send a
diff via the merge PATCH.
Resources
GET /api/projects/:id/resources
PATCH /api/projects/:id/resources
POST /api/projects/:id/resourcesRead or update the CPU/RAM/disk allotment (separately for production and build), sleep behavior, and listen
port. POST is an alias of PATCH. All fields are optional; send only what you want to change.
const result = await ship.projects.updateResources("proj_123", {
production: {
cpuCores: 1,
memoryMb: 1024,
},
sleepMode: "always_on",
});
console.log(result);curl -X PATCH "$OPENSHIP_URL/api/projects/proj_123/resources" \
-H "Authorization: Bearer $OPENSHIP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"production":{"cpuCores":1,"memoryMb":1024},"sleepMode":"always_on"}'Prop
Type
Sleep mode also has its own shortcut route: POST /api/projects/:id/sleep-mode.
Create an environment
POST /api/projects/:id/environmentsProp
Type
Folder uploads
See Source files to upload a folder or deploy generated code.
Transfer to / from cloud
POST /api/projects/:id/transfer/to-cloud † self-hosted only
POST /api/projects/:id/transfer/to-self-hosted † self-hosted onlyPromote a local project to Openship Cloud, or pull a cloud project back to this instance. These are
meaningless on the SaaS (it is the cloud) and return 404 there. Both require project:admin.
const result = await ship.projects.transferToCloud("proj_123");
console.log(result);curl -X POST "$OPENSHIP_URL/api/projects/proj_123/transfer/to-cloud" \
-H "Authorization: Bearer $OPENSHIP_TOKEN"Retry routing with logs
Routing repair can include SSH and certificate issuance. Use the streaming operation to follow its progress without the ordinary request timeout:
for await (const event of ship.projects.retryRoutingStream("proj_123")) {
console.log(event.event, event.data);
}The HTTP equivalent is POST /api/projects/:id/routing/retry/stream with an SSE response. Log events
contain the server's messages. A terminal complete event carries status: "completed" or
status: "failed"; a disconnected stream alone does not establish success. Cancelling the stream
stops log delivery while an accepted repair continues under the project's runtime lock.
The first session event includes a sessionId. Pass it to
ship.projects.retryRoutingStream(id, { sessionId }), or use
GET /api/projects/:id/routing/retry/stream?sessionId=..., to replay retained logs and follow the
same repair. Reconnecting requires project:read and never starts another repair. The project info
response includes the latest session as project.routingRetry so the dashboard can restore the log
after a refresh. Completed logs are retained in the running engine for up to 30 minutes, subject to
the retention limit; they do not survive an API restart.
Starting requires project:write. Concurrent start requests join the project's active repair.
For an uncertain start response, reuse options.idempotencyKey (the idempotencyKey query parameter
over HTTP) so a repeated request also rejoins a completed, retained session. Use a new key only
when intentionally starting another repair.
Errors
400 — validation failed
A field is missing or out of range (e.g. name empty, a cpuCores above 4, or git/link without
owner/repo). The response names the offending field.
403 — insufficient permission
Your token or role lacks the route's tag — deleting needs project:admin, writes need project:write.
See the permission model.
404 on a per-project route
The :id doesn't belong to your organization (or was deleted). A self-hosted-only route (marked †)
also returns 404 when called on Openship Cloud. List your projects with GET /api/projects to get valid
IDs.
Operations
Lifecycle
| Operation | SDK | REST API |
|---|---|---|
| Read the project overview and summary counts. | projects.getHome() | GET /api/projects/homeproject:list |
| Create a project record from explicit configuration. | projects.create(input) | POST /api/projectsproject:write |
| Create or select a project during source import. | projects.ensure(input) | POST /api/projects/ensureproject:write |
| Return a page of accessible projects. | projects.list(input?) | GET /api/projectsproject:list |
| Read one project and its active deployment ID. | projects.get(id) | GET /api/projects/:idproject:read |
| Update project configuration. | projects.update(id, input) | PATCH /api/projects/:idproject:write |
| Tear down the project and remove its records; inspect the returned steps. | projects.remove(id, input?) | DELETE /api/projects/:idproject:admin |
| Enable the project runtime. | projects.enable(id) | POST /api/projects/:id/enableproject:write |
| Disable the project runtime. | projects.disable(id) | POST /api/projects/:id/disableproject:write |
| Inspect resources that project deletion would affect. | projects.deletionPreview(id) | GET /api/projects/:id/deletion-previewproject:read |
Source and deployment
| Operation | SDK | REST API |
|---|---|---|
| Read the project’s detailed information. | projects.getInfo(id) | GET /api/projects/:id/infoproject:read |
| Read linked Git source information. | projects.getGitInfo(id) | GET /api/projects/:id/gitproject:read |
| List branches available from the linked source. | projects.listBranches(id, input?) | GET /api/projects/:id/branchesproject:read |
| Link a Git repository to the project. | projects.linkRepo(id, input) | POST /api/projects/:id/git/linkproject:write |
| Configure a release image as the deployment source. | projects.setReleaseImageSource(id, input) | PUT /api/projects/:id/release-image-sourceproject:write |
| Enable or disable automatic deployment triggers. | projects.setAutoDeploy(id, input) | POST /api/projects/:id/auto-deployproject:write |
| Choose the domain used for the project webhook. | projects.setWebhookDomain(id, input) | POST /api/projects/:id/webhook-domainproject:write |
| Read a page of the project’s deployments. | projects.listDeployments(id, input?) | GET /api/projects/:id/deploymentsproject:deployment:list |
| Read the project’s current deployment session. | projects.deploymentSession(id) | POST /api/projects/:id/deployment-sessionproject:read |
| Clear authorized build cache for the project. | projects.clearBuildCache(id) | POST /api/projects/:id/clear-buildproject:admin · Self-hosted |
| Inspect retained rollback capacity. | projects.getRollbackCapacity(id) | GET /api/projects/:id/rollback-capacityproject:read |
| Check deployment port requirements. | projects.checkPorts(id) | POST /api/projects/:id/port-checkproject:read |
| Check the configured build output. | projects.checkOutput(id) | POST /api/projects/:id/output-checkproject:read |
| Read actions that need a deployment or routing decision. | projects.getPendingActions(id) | GET /api/projects/:id/pending-actionsproject:read |
| Compare source and deployed commit status. | projects.getCommitStatus(id) | GET /api/projects/:id/commit-statusproject:read |
| Select the default Git branch. | projects.setBranch(id, input) | POST /api/projects/:id/branchproject:write |
| Scan an allowed local source directory. | projects.scanLocal(input) | POST /api/projects/scanproject:write · Self-hosted |
| Import an allowed local project. | projects.importLocal(input) | POST /api/projects/importproject:write · Self-hosted |
| List discoverable local projects. | projects.listLocal() | GET /api/projects/localproject:list · Self-hosted |
Cluster scaling
| Operation | SDK | REST API |
|---|---|---|
| Read the selected cluster, active release and observed replicas. | projects.getClusterWorkload(id) | GET /api/projects/:id/clusterproject:read · Self-hosted |
| Save a ready self-hosted cluster target, application configuration and optional shared-file mounts. Requires fleet administration and the current project timestamp. | projects.setClusterTarget(id, input) | PATCH /api/projects/:id/clusterproject:write · Self-hosted |
| Apply a replica count through a deployment using the active immutable image. Requires active deployment and project timestamp preconditions. | projects.scaleClusterWorkload(id, input) | POST /api/projects/:id/cluster/scaleproject:write · Self-hosted |
Cluster databases
| Operation | SDK | REST API |
|---|---|---|
| List the project's operator-managed databases and saved observations. Available on self-hosted OpenShip. | projects.listClusterDatabases(id) | GET /api/projects/:id/cluster/databasesproject:read · Self-hosted |
| List eligible PostgreSQL and Redis backup imports from this project. Requires project administrator access; source backups and running services are preserved. | projects.listClusterDatabaseImports(id) | GET /api/projects/:id/cluster/databases/importsproject:read · Self-hosted |
| Read saved database progress, or refresh the operator's instances, volumes and archive status with observe. Does not start or retry setup. | projects.getClusterDatabase(id, input) | POST /api/projects/:id/cluster/databases/inspectproject:read · Self-hosted |
| Create PostgreSQL or Redis on a ready server cluster with a stable request UUID. Recover a database archive, import an existing project backup or create an upgraded PostgreSQL copy. Requires fleet administration; the source is preserved. | projects.createClusterDatabase(id, input) | POST /api/projects/:id/cluster/databasesproject:write · Self-hosted |
| Apply supported resource, replica or backup settings with the current sequence. Redis shard changes require explicit review and a verified backup; PostgreSQL major upgrades use a new database copy. | projects.updateClusterDatabase(id, input) | PATCH /api/projects/:id/cluster/databasesproject:write · Self-hosted |
| Resume a failed or interrupted database operation from its saved intent. The previous controller is fenced and accepted operator resources are inspected before reuse. | projects.retryClusterDatabase(id, input) | POST /api/projects/:id/cluster/databases/retryproject:write · Self-hosted |
| Request a PostgreSQL or Redis archive at the configured S3 destination. Native controllers and Jobs own execution; retry reuses the accepted backup request. | projects.backupClusterDatabase(id, input) | POST /api/projects/:id/cluster/databases/backupproject:write · Self-hosted |
| Stop a disconnected database with name and sequence confirmation. deleteData=false retains persistent disks; true explicitly reclaims owned storage. S3 archives are kept. | projects.removeClusterDatabase(id, input) | DELETE /api/projects/:id/cluster/databasesproject:write · Self-hosted |
| Save a private database URL in the project environment, or explicitly replace another managed database binding using both current sequences. Set envKey to null to disconnect. Redeploy to apply the reviewed change. | projects.connectClusterDatabase(id, input) | POST /api/projects/:id/cluster/databases/connectproject:write · Self-hosted |
| Subscribe to replayable database progress snapshots over SSE. Reconnection only reads saved state and never repeats a mutation. | projects.streamClusterDatabaseEvents(id, options?) | GET /api/projects/:id/cluster/databases/streamproject:read · Self-hosted |
Shared files
| Operation | SDK | REST API |
|---|---|---|
| Read shared file volumes, independent copies, attachment health and backup status. | projects.listClusterVolumes(id) | GET /api/projects/:id/cluster/volumesproject:read · Self-hosted |
| List project file archives, including backups whose original volume was deleted. | projects.listClusterVolumeBackups(id) | GET /api/projects/:id/cluster/volumes/backupsproject:read · Self-hosted |
| Create a replicated shared volume or restore an archive into a new one using a stable request ID. Attach it through cluster configuration and deploy. | projects.createClusterVolume(id, input) | POST /api/projects/:id/cluster/volumesproject:write · Self-hosted |
| Grow a shared volume using its current resourceVersion. Observe status until expansion completes; shrinking is unsupported. | projects.resizeClusterVolume(id, input) | PATCH /api/projects/:id/cluster/volumesproject:write · Self-hosted |
| Save a file-volume backup to the cluster's configured external destination using a stable request ID. Pause application writes when consistency requires it. | projects.backupClusterVolume(id, input) | POST /api/projects/:id/cluster/volumes/backupproject:write · Self-hosted |
| Schedule hourly or daily file backups with a retained archive count, or choose manual to stop the schedule. | projects.scheduleClusterVolumeBackups(id, input) | PATCH /api/projects/:id/cluster/volumes/backupsproject:write · Self-hosted |
| Permanently delete a project file archive after confirming its exact backup name. The source volume is preserved. | projects.removeClusterVolumeBackup(id, input) | DELETE /api/projects/:id/cluster/volumes/backupsproject:write · Self-hosted |
| Delete an unmounted volume with resourceVersion, name and data-deletion confirmation. Disconnect and redeploy first; external archives are preserved. | projects.removeClusterVolume(id, input) | DELETE /api/projects/:id/cluster/volumesproject:write · Self-hosted |
| Subscribe to current file-volume and archive snapshots over SSE without repeating mutations on reconnect. | projects.streamClusterVolumeEvents(id, options?) | GET /api/projects/:id/cluster/volumes/streamproject:read · Self-hosted |
Environments and settings
| Operation | SDK | REST API |
|---|---|---|
| List the project’s environments. | projects.listEnvironments(id) | GET /api/projects/:id/environmentsproject:read |
| Create an environment from the supplied configuration. | projects.createEnvironment(id, input) | POST /api/projects/:id/environmentsproject:write |
| Read environment variables with secrets masked. | projects.listEnvVars(id, input?) | GET /api/projects/:id/envproject:read |
| Upsert and delete named environment variables. | projects.mergeEnvVars(id, input) | PATCH /api/projects/:id/envproject:write |
| Read resource limits and sleep settings. | projects.getResources(id) | GET /api/projects/:id/resourcesproject:read |
| Change resource allocations. | projects.updateResources(id, input) | PATCH /api/projects/:id/resourcesproject:write |
| Set the project’s sleep behavior. | projects.setSleepMode(id, input) | POST /api/projects/:id/sleep-modeproject:write |
| Change deployment options. | projects.setOptions(id, input) | POST /api/projects/:id/optionsproject:write |
| Read whether a project clone token is configured. | projects.getCloneToken(id) | GET /api/projects/:id/clone-tokenproject:read |
| Set, rotate, or clear the project clone token. | projects.updateCloneToken(id, input) | PATCH /api/projects/:id/clone-tokenproject:admin |
Routing and monitoring
| Operation | SDK | REST API |
|---|---|---|
| Repair project routing and verify pending domains and HTTPS. | projects.retryRouting(id) | POST /api/projects/:id/routing/retryproject:write |
| Repair routing with live logs and a terminal success or failure event. | projects.retryRoutingStream(id, options?) | POST /api/projects/:id/routing/retry/streamproject:write |
| Read recent runtime log entries. | projects.runtimeLogs(id, input?) | GET /api/projects/:id/logsproject:read |
| List the project’s edge route rules. | projects.listRouteRules(id) | GET /api/projects/:id/route-rulesproject:read · Self-hosted |
| Add an edge route rule. | projects.createRouteRule(id, input) | POST /api/projects/:id/route-rulesproject:write · Self-hosted |
| Update an edge route rule using its typed input. | projects.updateRouteRule(id, input) | PATCH /api/projects/:id/route-rules/:ruleIdproject:write · Self-hosted |
| Remove the specified edge route rule. | projects.removeRouteRule(id, input) | DELETE /api/projects/:id/route-rules/:ruleIdproject:write · Self-hosted |
| Read open and resolved health incidents. | projects.getIncidents(id) | GET /api/projects/:id/incidentsproject:read · Self-hosted |
| Attach a domain to the project. | projects.connectDomain(id, input) | POST /api/projects/:id/connectproject:write |
| Read the project’s edge configuration. | projects.getEdgeConfig(id) | GET /api/projects/:id/edge-configproject:read · Self-hosted |
| Obtain an authorized server-log stream token. | projects.getServerLogStreamToken(id, input?) | GET /api/projects/:id/server-logs/stream-tokenproject:read |
| Read recent request/server logs. | projects.recentServerLogs(id, input?) | GET /api/projects/:id/server-logs/recentproject:read |
| Stream project runtime log events. | projects.streamRuntimeLogs(id, input?, options?) | GET /api/projects/:id/logs/streamproject:read |
| Stream project request/server log events. | projects.streamServerLogs(id, input?, options?) | GET /api/projects/:id/server-logs/streamproject:read |
Connections, storage, and apps
| Operation | SDK | REST API |
|---|---|---|
| List the project’s outgoing resource connections. | projects.listConnections(id) | GET /api/projects/:id/connectionsproject:read |
| List writable projects and apps for a connection, including cluster IDs for native database placement. | projects.listConnectionCandidates(id) | GET /api/projects/:id/connections/candidatesproject:write |
| List projects consuming this project’s outputs. | projects.listConnectionConsumers(id) | GET /api/projects/:id/connections/consumersproject:read |
| Connect an output to a target secret environment variable. Native database outputs require source and target write access and the same organization and cluster; ownership stays with the source. | projects.createConnection(id, input) | POST /api/projects/:id/connectionsproject:write |
| Save a group of compatible resource connections and encrypted environment values in one transaction. Native database links reconcile private namespace access before reporting success; a failed reconciliation can be retried with the same inputs. | projects.connectBundle(id, input) | POST /api/projects/:id/connections/bundleproject:write |
| Remove a resource connection. | projects.removeConnection(id, input) | DELETE /api/projects/:id/connections/:linkIdproject:admin |
| Read the project’s object-storage binding. | projects.getStorage(id) | GET /api/projects/:id/storageproject:read |
| Bind a configured storage bucket. | projects.bindStorage(id, input) | POST /api/projects/:id/storageproject:write |
| Remove the storage binding. | projects.unbindStorage(id) | DELETE /api/projects/:id/storageproject:admin |
Cloud transfer
| Operation | SDK | REST API |
|---|---|---|
| Request a Cloud transfer; fixed scopes currently require unavailable tenant mapping. | projects.transferToCloud(id) | POST /api/projects/:id/transfer/to-cloudproject:admin · Self-hosted |
| Request a transfer from Cloud; fixed-scope Cloud mapping limits apply. | projects.transferToSelfHosted(id) | POST /api/projects/:id/transfer/to-self-hostedproject:admin · Self-hosted |
HTTP endpoints
| Operation | SDK | REST API |
|---|---|---|
| Update resources (POST alias of the PATCH). | HTTP only | POST /api/projects/:id/resourcesproject:write |
| Read edge readiness for the project. | HTTP only | GET /api/projects/:id/routing/edge-statusproject:read |
| Replay and follow an existing routing repair using its sessionId; never starts a repair. | projects.retryRoutingStream(id, { sessionId }) | GET /api/projects/:id/routing/retry/streamproject:read |
| Answer an edge setup prompt for the project. | HTTP only | POST /api/projects/:id/routing/ensure-edge/respondproject:write |
| Stream edge setup for the project. | HTTP only | POST /api/projects/:id/routing/ensure-edge/streamproject:write |