Base URL:
http://<server-ip>:8000Interactive Docs:GET /docs(Swagger UI) ·GET /redoc(ReDoc)
- Overview
- CORS Configuration
- Authentication
- Deploy
- Health Check
- Data Models Reference
- Deployment Status Lifecycle
- Error Reference
- Frontend Integration Guide
cploy is a self-hosted deployment platform. The API lets you:
- Register an account and log in to receive a JWT
- Deploy a Vite + React or Node.js app from a GitHub repo (runs asynchronously in the background)
- Poll the deployment status until it completes
- List all your deployed projects
- Delete a deployment (tears down Docker container, nginx config, and files)
Important
Each user is limited to 2 active deployments at a time. Deleting a deployment or having a failed one frees up a slot.
All deploy endpoints are protected — they require a valid JWT in the Authorization header.
sequenceDiagram
participant FE as Frontend
participant API as cploy API
participant BG as Background Worker
FE->>API: POST /auth/register
API-->>FE: { id, username }
FE->>API: POST /auth/login
API-->>FE: { access_token }
FE->>API: GET /deploy/my-projects (with Bearer token)
API-->>FE: [ ...deployments ]
FE->>API: POST /deploy/vite/react (with Bearer token)
API->>BG: Start deployment task
API-->>FE: { deployment_id, status: "pending" }
FE->>API: POST /deploy/node/javascript (with Bearer token)
API->>BG: Start Node deployment task
API-->>FE: { deployment_id, status: "pending" }
loop Poll every 3-5 seconds
FE->>API: GET /deploy/{id}/status (with Bearer token)
API-->>FE: { status: "running" | "success" | "failed" }
end
FE->>API: DELETE /deploy/{id} (with Bearer token)
API->>BG: Start delete task
API-->>FE: { deployment_id, status: "deleting" }
The API allows requests from these origins:
| Origin | Purpose |
|---|---|
http://localhost:5173 |
Vite dev server |
https://dev-saurabh-k.xyz |
Production domain |
https://www.dev-saurabh-k.xyz |
www subdomain |
https://cploy.dev-saurabh-k.xyz |
cploy subdomain |
All methods, headers, and credentials are allowed. Requests from other origins will be blocked by the browser.
| Detail | Value |
|---|---|
| Method | JWT Bearer Token (OAuth2 Password Flow) |
| Token Lifetime | 60 minutes |
| Algorithm | HS256 |
| Header Format | Authorization: Bearer <token> |
Important
Store the access_token securely on the client (e.g. in memory or httpOnly cookie). Include it in the Authorization header for all protected endpoints.
Create a new user account.
| Property | Value |
|---|---|
| URL | /auth/register |
| Method | POST |
| Auth Required | ❌ No |
| Content-Type | application/json |
| Field | Type | Required | Description |
|---|---|---|---|
username |
string |
✅ | Unique username (max 50 characters) |
password |
string |
✅ | Plain-text password (hashed server-side with bcrypt) |
{
"username": "john",
"password": "securePassword123"
}| Field | Type | Description |
|---|---|---|
id |
integer |
The newly created user's ID |
username |
string |
The registered username |
{
"id": 1,
"username": "john"
}| Status | Condition | Response Body |
|---|---|---|
400 Bad Request |
Username already taken | {"detail": "Username already taken"} |
422 Unprocessable Entity |
Missing/invalid fields | Validation error details |
const response = await fetch("/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: "john",
password: "securePassword123",
}),
});
if (response.status === 201) {
const user = await response.json();
console.log("Registered:", user.username);
} else if (response.status === 400) {
const error = await response.json();
alert(error.detail); // "Username already taken"
}Authenticate with username and password. Returns a JWT access token.
| Property | Value |
|---|---|
| URL | /auth/login |
| Method | POST |
| Auth Required | ❌ No |
| Content-Type | application/x-www-form-urlencoded |
Warning
This endpoint uses form data (application/x-www-form-urlencoded), not JSON. This follows the OAuth2 password flow standard.
| Field | Type | Required | Description |
|---|---|---|---|
username |
string |
✅ | Registered username |
password |
string |
✅ | User's password |
username=john&password=securePassword123
| Field | Type | Description |
|---|---|---|
access_token |
string |
JWT token to use in Authorization header |
token_type |
string |
Always "bearer" |
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huIiwiZXhwIjoxNjk...",
"token_type": "bearer"
}| Status | Condition | Response Body |
|---|---|---|
401 Unauthorized |
Wrong username or password | {"detail": "Invalid username or password"} |
422 Unprocessable Entity |
Missing fields | Validation error details |
const response = await fetch("/auth/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
username: "john",
password: "securePassword123",
}),
});
if (response.ok) {
const data = await response.json();
// Store token for subsequent requests
localStorage.setItem("token", data.access_token);
} else {
const error = await response.json();
alert(error.detail); // "Invalid username or password"
}Deployments are asynchronous. When you start a deploy:
- The API creates a deployment record and returns immediately with
202 Accepted - The actual build + deploy runs in a background task on the server
- The frontend polls the status endpoint to track progress
The deployment pipeline runs these steps sequentially:
- Create deployment directory on the server
- Generate
docker-compose.yml - Write supplied environment variables to the deployment's
.envfile - Clone the GitHub repository
- Run
docker compose up -d --build(which loads.env) - Configure nginx reverse proxy
If any step fails, the deployment is marked failed with the error details.
Tip
Multiple deployments can run concurrently. Each runs in its own background task, so users don't have to wait for each other.
Ports are automatically assigned by the server — you don't need to provide one. The server picks the next available port from the range 10000–40000 by checking the database for ports already in use by active deployments. Ports from deleted or failed deployments are recycled.
The assigned port is visible in the response when you poll GET /deploy/{id}/status or GET /deploy/my-projects.
Important
Each user can have a maximum of 2 active deployments at any time. Active means any deployment that is not in deleted or failed status (i.e. pending, running, success, or deleting all count).
If the limit is reached, either deploy route returns:
POST /deploy/vite/reactPOST /deploy/node/javascript
// 403 Forbidden
{
"detail": "Deployment limit reached. Maximum 2 active deployments per user."
}The route-level behavior is the same for both deploy endpoints.
How to free up a slot:
- Delete an existing deployment via
DELETE /deploy/{id} - A
faileddeployment does not count toward the limit
Start a new Node.js deployment. The request returns immediately while the deployment runs in the background.
| Property | Value |
|---|---|
| URL | /deploy/node/javascript |
| Method | POST |
| Auth Required | ✅ Yes — Authorization: Bearer <token> |
| Content-Type | application/json |
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer <access_token> |
✅ |
Content-Type |
application/json |
✅ |
| Field | Type | Required | Description |
|---|---|---|---|
image_name |
string |
✅ | App name for the Docker container and subdomain: <image_name>.dev-saurabh-k.xyz. Must be 1–63 lowercase characters; start with a letter; and contain only lowercase letters, digits, or internal hyphens. |
repo_url |
string |
✅ | GitHub repository URL to clone (e.g. "https://github.com/user/repo.git") |
start_command |
string |
No | Optional shell command to start the app, for example npm start, node server.js, or pm2 start ecosystem.config.js |
environment_variables |
object |
No | Environment variables to provide to the container. Keys must be valid shell-style variable names; values must be strings. Defaults to {}. |
{
"image_name": "node-app",
"repo_url": "https://github.com/johndoe/node-api.git",
"start_command": "npm start",
"environment_variables": {
"PORT": "3000",
"NODE_ENV": "production"
}
}This route follows the same deployment lifecycle as Vite deployments, but it uses the project’s Node deployment scripts and Dockerfile before the app is started. The start_command is stored in the container environment as START_COMMAND, and the generated Compose configuration executes that value with sh -c. The Dockerfile CMD acts only as a fallback if no START_COMMAND is supplied.
Important
image_namemust be unique across all active deployments (including other users)repo_urlmust point to a valid public GitHub repository- User must have fewer than 2 active deployments
- Port is auto-assigned from the same range 10000–40000
- You may send up to 100 environment variables
| Field | Type | Description |
|---|---|---|
deployment_id |
integer |
Unique ID to poll for status |
status |
string |
Always "pending" at this point |
message |
string |
Human-readable instructions |
{
"deployment_id": 3,
"status": "pending",
"message": "Node.js deployment started. Poll /deploy/{id}/status for updates."
}| Status | Condition | Response Body |
|---|---|---|
400 Bad Request |
Repository is private or not found | {"detail": "Repository is private or does not exist. You must provide a public GitHub repository link."} |
401 Unauthorized |
Missing or invalid token | {"detail": "Invalid or expired token"} |
403 Forbidden |
User already has 2 active deployments | {"detail": "Deployment limit reached. Maximum 2 active deployments per user."} |
409 Conflict |
An active deployment already uses image_name |
{"detail": "An active deployment already uses this app name."} |
422 Unprocessable Entity |
Missing/invalid fields | Validation error details |
503 Service Unavailable |
All ports in range are in use | {"detail": "No available ports. Please try again later."} |
const token = localStorage.getItem("token");
const response = await fetch("/deploy/node/javascript", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
image_name: "node-app",
repo_url: "https://github.com/johndoe/node-api.git",
start_command: "npm start",
environment_variables: {
PORT: "3000",
NODE_ENV: "production",
},
}),
});
if (response.status === 202) {
const data = await response.json();
pollDeploymentStatus(data.deployment_id);
} else if (response.status === 403) {
const error = await response.json();
alert(error.detail);
}// 403 Forbidden
{
"detail": "Deployment limit reached. Maximum 2 active deployments per user."
}How to free up a slot:
- Delete an existing deployment via
DELETE /deploy/{id} - A
faileddeployment does not count toward the limit
List all deployments belonging to the authenticated user. Returns deployments sorted by newest first.
| Property | Value |
|---|---|
| URL | /deploy/my-projects |
| Method | GET |
| Auth Required | ✅ Yes — Authorization: Bearer <token> |
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer <access_token> |
✅ |
Returns an array of deployment objects. Empty array [] if the user has no deployments.
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
integer |
No | Deployment ID |
image_name |
string |
No | Docker image/container name |
port |
string |
No | Auto-assigned host port (10000–40000) |
repo_url |
string |
No | GitHub repository URL |
status |
string |
No | One of: pending, running, success, failed, deleting, deleted |
error_message |
string |
Yes | Error details (only when status is "failed") |
domain |
string |
Yes | Live domain URL (only when status is "success") |
[
{
"id": 2,
"image_name": "portfolio",
"port": "10006",
"repo_url": "https://github.com/john/portfolio.git",
"status": "success",
"error_message": null,
"domain": "portfolio.dev-saurabh-k.xyz"
},
{
"id": 1,
"image_name": "myapp",
"port": "10005",
"repo_url": "https://github.com/john/myapp.git",
"status": "failed",
"error_message": "docker_compose: Error response from daemon: ...",
"domain": null
}
]| Status | Condition | Response Body |
|---|---|---|
401 Unauthorized |
Missing or invalid token | {"detail": "Invalid or expired token"} |
const token = localStorage.getItem("token");
const response = await fetch("/deploy/my-projects", {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const projects = await response.json();
projects.forEach((project) => {
console.log(`${project.image_name} — ${project.status}`);
if (project.domain) {
console.log(` Live at: https://${project.domain}`);
}
if (project.error_message) {
console.log(` Error: ${project.error_message}`);
}
});
} else if (response.status === 401) {
window.location.href = "/login";
}Start a new Vite + React deployment. The request returns immediately while the deployment runs in the background.
| Property | Value |
|---|---|
| URL | /deploy/vite/react |
| Method | POST |
| Auth Required | ✅ Yes — Authorization: Bearer <token> |
| Content-Type | application/json |
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer <access_token> |
✅ |
Content-Type |
application/json |
✅ |
| Field | Type | Required | Description |
|---|---|---|---|
image_name |
string |
✅ | App name for the Docker container and subdomain: <image_name>.dev-saurabh-k.xyz. Must be 1–63 lowercase characters; start with a letter; and contain only lowercase letters, digits, or internal hyphens. |
repo_url |
string |
✅ | GitHub repository URL to clone (e.g. "https://github.com/user/repo.git") |
environment_variables |
object |
No | Environment variables to provide to the container. Keys must be valid shell-style variable names; values must be strings. Defaults to {}. |
{
"image_name": "myapp",
"repo_url": "https://github.com/johndoe/my-react-app.git",
"environment_variables": {
"API_URL": "https://api.example.com",
"FEATURE_FLAG": "enabled"
}
}The variables are written to /opt/deployCode/<image_name>/.env. The generated
Compose configuration uses that file as its env_file, so they are available to
the running container. They are not returned by deployment status endpoints.
Important
image_namemust be unique across all active deployments (including other users), because it becomes the Docker container name and nginx subdomain. Names are 1–63 characters, start with a lowercase letter, may include lowercase letters, digits, and internal hyphens, and cannot end with a hyphen.repo_urlmust contain a valid Vite + React project with apackage.jsonat the root- User must have fewer than 2 active deployments or the request will be rejected
- Port is auto-assigned from range 10000–40000 — do not send it in the request
- You may send up to 100 environment variables. Names must start with a letter or underscore and contain only letters, digits, and underscores. Values cannot contain line breaks.
| Field | Type | Description |
|---|---|---|
deployment_id |
integer |
Unique ID to poll for status |
status |
string |
Always "pending" at this point |
message |
string |
Human-readable instructions |
{
"deployment_id": 1,
"status": "pending",
"message": "Deployment started. Poll /deploy/{id}/status for updates."
}| Status | Condition | Response Body |
|---|---|---|
400 Bad Request |
Repository is private or not found | {"detail": "Repository is private or does not exist. You must provide a public GitHub repository link."} |
401 Unauthorized |
Missing or invalid token | {"detail": "Invalid or expired token"} |
401 Unauthorized |
No Authorization header |
{"detail": "Not authenticated"} |
403 Forbidden |
User already has 2 active deployments | {"detail": "Deployment limit reached. Maximum 2 active deployments per user."} |
409 Conflict |
An active deployment already uses image_name |
{"detail": "An active deployment already uses this app name."} |
422 Unprocessable Entity |
Missing/invalid fields | Validation error details |
503 Service Unavailable |
All ports in range are in use | {"detail": "No available ports. Please try again later."} |
const token = localStorage.getItem("token");
const response = await fetch("/deploy/vite/react", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
image_name: "myapp",
repo_url: "https://github.com/johndoe/my-react-app.git",
environment_variables: {
API_URL: "https://api.example.com",
FEATURE_FLAG: "enabled",
},
}),
});
if (response.status === 202) {
const data = await response.json();
// Start polling for status
pollDeploymentStatus(data.deployment_id);
} else if (response.status === 403) {
const error = await response.json();
alert(error.detail); // "Deployment limit reached..."
} else if (response.status === 401) {
window.location.href = "/login";
}Check the current status of a deployment. Users can only view their own deployments.
| Property | Value |
|---|---|
| URL | /deploy/{deployment_id}/status |
| Method | GET |
| Auth Required | ✅ Yes — Authorization: Bearer <token> |
| Parameter | Type | Required | Description |
|---|---|---|---|
deployment_id |
integer |
✅ | The deployment ID returned from the deploy endpoint |
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer <access_token> |
✅ |
| Field | Type | Nullable | Description |
|---|---|---|---|
id |
integer |
No | Deployment ID |
image_name |
string |
No | Docker image/container name |
port |
string |
No | Auto-assigned host port (10000–40000) |
repo_url |
string |
No | GitHub repository URL |
status |
string |
No | One of: pending, running, success, failed, deleting, deleted |
error_message |
string |
Yes | Error details (only when status is "failed") |
domain |
string |
Yes | Live domain URL (only when status is "success") |
Example — Pending / Running:
{
"id": 1,
"image_name": "myapp",
"port": "10005",
"repo_url": "https://github.com/johndoe/my-react-app.git",
"status": "running",
"error_message": null,
"domain": null
}Example — Success:
{
"id": 1,
"image_name": "myapp",
"port": "10005",
"repo_url": "https://github.com/johndoe/my-react-app.git",
"status": "success",
"error_message": null,
"domain": "myapp.dev-saurabh-k.xyz"
}Example — Failed:
{
"id": 1,
"image_name": "myapp",
"port": "10005",
"repo_url": "https://github.com/johndoe/my-react-app.git",
"status": "failed",
"error_message": "docker_compose: Error response from daemon: ...",
"domain": null
}Example — Deleted:
{
"id": 1,
"image_name": "myapp",
"port": "10005",
"repo_url": "https://github.com/johndoe/my-react-app.git",
"status": "deleted",
"error_message": null,
"domain": null
}| Status | Condition | Response Body |
|---|---|---|
401 Unauthorized |
Missing or invalid token | {"detail": "Invalid or expired token"} |
404 Not Found |
Deployment doesn't exist or belongs to another user | {"detail": "Deployment not found"} |
async function pollDeploymentStatus(deploymentId) {
const token = localStorage.getItem("token");
const poll = setInterval(async () => {
const response = await fetch(`/deploy/${deploymentId}/status`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.status === 401) {
clearInterval(poll);
window.location.href = "/login";
return;
}
const data = await response.json();
switch (data.status) {
case "pending":
console.log("⏳ Waiting to start...");
break;
case "running":
console.log("🚀 Deployment in progress...");
break;
case "success":
clearInterval(poll);
console.log(`✅ Live at: https://${data.domain}`);
break;
case "failed":
clearInterval(poll);
console.error(`❌ Failed: ${data.error_message}`);
break;
case "deleting":
console.log("🗑️ Deleting...");
break;
case "deleted":
clearInterval(poll);
console.log("🗑️ Deployment deleted");
break;
}
}, 3000); // Poll every 3 seconds
}Delete a deployment. Tears down the Docker container and images, removes nginx config, and deletes all files from the server. Runs asynchronously as a background task.
| Property | Value |
|---|---|
| URL | /deploy/{deployment_id} |
| Method | DELETE |
| Auth Required | ✅ Yes — Authorization: Bearer <token> |
| Parameter | Type | Required | Description |
|---|---|---|---|
deployment_id |
integer |
✅ | The deployment ID to delete |
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer <access_token> |
✅ |
The delete_deployment.sh script runs these cleanup steps:
- Remove nginx symlink from
sites-enabled - Remove nginx config from
sites-available - Test and reload nginx
- Run
docker compose down --rmi all --remove-orphans(stops containers, removes images) - Delete the entire deployment directory (
/opt/deployCode/<image_name>)
| Field | Type | Description |
|---|---|---|
deployment_id |
integer |
The deployment being deleted |
status |
string |
"deleting" — deletion is in progress |
message |
string |
Human-readable instructions |
{
"deployment_id": 1,
"status": "deleting",
"message": "Deletion started. Poll /deploy/{id}/status for updates."
}| Status | Condition | Response Body |
|---|---|---|
401 Unauthorized |
Missing or invalid token | {"detail": "Invalid or expired token"} |
404 Not Found |
Deployment doesn't exist or belongs to another user | {"detail": "Deployment not found"} |
409 Conflict |
Deployment is already being deleted | {"detail": "Deployment is already being deleted"} |
410 Gone |
Deployment has already been deleted | {"detail": "Deployment has already been deleted"} |
async function deleteDeployment(deploymentId) {
const token = localStorage.getItem("token");
const response = await fetch(`/deploy/${deploymentId}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {
const data = await response.json();
console.log(data.message);
// Poll for deletion completion
pollDeploymentStatus(deploymentId);
} else {
const error = await response.json();
switch (response.status) {
case 401:
window.location.href = "/login";
break;
case 404:
alert("Deployment not found");
break;
case 409:
alert("Already being deleted — please wait");
break;
case 410:
alert("This deployment was already deleted");
break;
}
}
}Simple health check endpoint. No authentication required.
| Property | Value |
|---|---|
| URL | / |
| Method | GET |
| Auth Required | ❌ No |
{
"status": "working"
}| Field | Type | Description |
|---|---|---|
id |
integer |
Auto-incrementing primary key |
username |
string(50) |
Unique username |
hashed_password |
string(255) |
Bcrypt-hashed password (never exposed via API) |
created_at |
datetime |
UTC timestamp of account creation |
| Field | Type | Description |
|---|---|---|
id |
integer |
Auto-incrementing primary key |
user_id |
integer |
Foreign key → users.id |
image_name |
string(100) |
Docker image/container name |
port |
string(10) |
Auto-assigned host port (range 10000–40000) |
repo_url |
string(500) |
GitHub repository URL |
status |
string(20) |
pending | running | success | failed | deleting | deleted |
error_message |
text |
Error details (null if no error) |
domain |
string(200) |
Generated domain (null until success) |
created_at |
datetime |
UTC timestamp of deployment creation |
updated_at |
datetime |
UTC timestamp of last status change |
stateDiagram-v2
[*] --> pending: POST /deploy/vite/react
pending --> running: Background task picks up
running --> success: All 5 steps pass
running --> failed: Any step errors
success --> deleting: DELETE /deploy/{id}
failed --> deleting: DELETE /deploy/{id}
deleting --> deleted: Cleanup completes
deleting --> failed: Cleanup errors
deleted --> [*]
| Status | Meaning | error_message |
domain |
Counts toward limit? |
|---|---|---|---|---|
pending |
Created, waiting for background task to start | null |
null |
✅ Yes |
running |
Background task is executing the pipeline | null |
null |
✅ Yes |
success |
All steps completed — app is live | null |
"<image_name>.dev-saurabh-k.xyz" |
✅ Yes |
failed |
A step in the pipeline errored out | Contains error details | null |
❌ No |
deleting |
Delete script is running | null |
null |
✅ Yes |
deleted |
Fully cleaned up | null |
null |
❌ No |
All error responses use this format:
{
"detail": "Human-readable error message"
}| Code | Meaning | When |
|---|---|---|
200 OK |
Request succeeded | Login, status check, list projects, delete |
201 Created |
Resource created | Registration |
202 Accepted |
Accepted for background processing | Deploy started |
400 Bad Request |
Client error | Duplicate username |
401 Unauthorized |
Authentication failed | Missing/invalid/expired token, wrong credentials |
403 Forbidden |
Limit exceeded | User has 2 active deployments |
404 Not Found |
Resource not found | Deployment ID doesn't exist or belongs to another user |
409 Conflict |
Conflicting active app name or action already in progress | An active deployment already uses the app name, or the deployment is already being deleted |
410 Gone |
Resource already removed | Deployment was already deleted |
422 Unprocessable Entity |
Validation failed | Missing required fields, wrong data types |
503 Service Unavailable |
No ports available | All ports in range 10000–40000 are in use |
When request validation fails, the response body includes detailed field-level errors:
{
"detail": [
{
"loc": ["body", "image_name"],
"msg": "Field required",
"type": "missing"
}
]
}// ─── auth.js ─────────────────────────────────────────────
const API_BASE = "http://<server-ip>:8000";
export async function register(username, password) {
const res = await fetch(`${API_BASE}/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!res.ok) throw await res.json();
return res.json();
}
export async function login(username, password) {
const res = await fetch(`${API_BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ username, password }),
});
if (!res.ok) throw await res.json();
const data = await res.json();
localStorage.setItem("token", data.access_token);
return data;
}
export function getToken() {
return localStorage.getItem("token");
}
export function logout() {
localStorage.removeItem("token");
}
export function authHeaders() {
return { Authorization: `Bearer ${getToken()}` };
}// ─── deploy.js ───────────────────────────────────────────
import { authHeaders } from "./auth.js";
const API_BASE = "http://<server-ip>:8000";
/** List all projects for the current user */
export async function getMyProjects() {
const res = await fetch(`${API_BASE}/deploy/my-projects`, {
headers: authHeaders(),
});
if (!res.ok) throw await res.json();
return res.json(); // DeployStatusResponse[]
}
/** Start a new deployment (port is auto-assigned by the server) */
export async function startDeploy(imageName, repoUrl, environmentVariables = {}) {
const res = await fetch(`${API_BASE}/deploy/vite/react`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...authHeaders(),
},
body: JSON.stringify({
image_name: imageName,
repo_url: repoUrl,
environment_variables: environmentVariables,
}),
});
if (!res.ok) throw await res.json();
return res.json(); // { deployment_id, status, message }
}
/** Get status of a single deployment */
export async function getDeployStatus(deploymentId) {
const res = await fetch(`${API_BASE}/deploy/${deploymentId}/status`, {
headers: authHeaders(),
});
if (!res.ok) throw await res.json();
return res.json();
}
/** Delete a deployment */
export async function deleteDeployment(deploymentId) {
const res = await fetch(`${API_BASE}/deploy/${deploymentId}`, {
method: "DELETE",
headers: authHeaders(),
});
if (!res.ok) throw await res.json();
return res.json(); // { deployment_id, status, message }
}
/**
* Poll deployment status until it reaches a terminal state.
* @param {number} deploymentId
* @param {function} onUpdate - Called with status object on each poll
* @param {number} intervalMs - Poll interval (default 3000ms)
* @returns {Promise} Resolves with final status object
*/
export function watchDeploy(deploymentId, onUpdate, intervalMs = 3000) {
const terminalStatuses = ["success", "failed", "deleted"];
return new Promise((resolve, reject) => {
const poll = setInterval(async () => {
try {
const status = await getDeployStatus(deploymentId);
onUpdate(status);
if (terminalStatuses.includes(status.status)) {
clearInterval(poll);
resolve(status);
}
} catch (err) {
clearInterval(poll);
reject(err);
}
}, intervalMs);
});
}import { login } from "./auth";
import { getMyProjects, startDeploy, deleteDeployment, watchDeploy } from "./deploy";
// ── List all projects ──
async function showProjects() {
const projects = await getMyProjects();
console.log(`You have ${projects.length} deployment(s)`);
projects.forEach((p) => {
console.log(` ${p.image_name} — ${p.status} (port: ${p.port}) ${p.domain ? `→ ${p.domain}` : ""}`);
});
}
// ── Deploy a new app (port is auto-assigned) ──
async function handleDeploy() {
await login("john", "securePassword123");
try {
const { deployment_id } = await startDeploy(
"my-portfolio",
"https://github.com/john/portfolio.git",
{ API_URL: "https://api.example.com" }
);
const result = await watchDeploy(deployment_id, (status) => {
console.log(`Status: ${status.status}`);
// Update your UI progress indicator here
});
if (result.status === "success") {
alert(`🎉 Live at https://${result.domain}`);
} else {
alert(`❌ Error: ${result.error_message}`);
}
} catch (err) {
if (err.detail?.includes("Deployment limit")) {
alert("You already have 2 active deployments. Delete one first.");
}
}
}
// ── Delete a deployment ──
async function handleDelete(deploymentId) {
try {
await deleteDeployment(deploymentId);
const result = await watchDeploy(deploymentId, (status) => {
console.log(`Deleting: ${status.status}`);
});
console.log("Deployment deleted successfully");
} catch (err) {
alert(err.detail);
}
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/ |
❌ | Health check |
POST |
/auth/register |
❌ | Create account |
POST |
/auth/login |
❌ | Login → JWT token |
GET |
/deploy/my-projects |
✅ | List user's deployments |
POST |
/deploy/vite/react |
✅ | Start Vite deployment (max 2) |
POST |
/deploy/node/javascript |
✅ | Start Node.js deployment (max 2) |
GET |
/deploy/{id}/status |
✅ | Poll deployment status |
DELETE |
/deploy/{id} |
✅ | Delete deployment |