# MarshHub — Complete Reference > Static site hosting for Mars IT School. Deploy to name.marshub.uz with one command. ## Overview MarshHub is a hosting platform for Mars IT School students (2,300+). Students deploy static sites via CLI — each gets a subdomain like `name.marshub.uz` with automatic HTTPS. **Stack**: Node.js CLI + Express API + SQLite + Caddy (TLS) + Telegram bot --- ## Installation ```bash # Use without installing npx marshub # Or install globally npm i -g marshub ``` **Requirements**: Node.js >= 18 --- ## Authentication Login uses Mars ID with confirmation in the Telegram bot `@marshubbot`: ```bash marshub login ``` 1. CLI generates a session ID, registers it on server 2. Opens `https://t.me/marshubbot?start=auth_` in browser 3. User presses "Start" in Telegram 4. CLI polls `GET /api/auth/poll?session=` every 2 seconds (max 5 min) 5. On confirmation, server returns API key 6. Key saved to `~/.marshubrc` as `{"token":"mh_...","api":"https://marshub.uz"}` API key is valid for 90 days. All subsequent requests use `Authorization: Bearer `. --- ## CLI Commands ### `marshub login` Authenticate via Telegram. Opens @marshubbot deep link, waits for confirmation via polling. Saves API key to `~/.marshubrc`. **Exit codes**: 0 (success), 1 (timeout/error) ### `marshub deploy [--name ]` Deploy the current working directory as a static site. **Process**: 1. Reads token from `~/.marshubrc` 2. Creates tar.gz of current directory 3. Auto-excludes: `node_modules/`, `.git/`, `.env`, `.DS_Store`, `__pycache__/` 4. Uploads via `POST /api/deploy` (multipart: `file` + optional `name`) 5. Server validates ownership, limits, blocked extensions, symlinks 6. Extracts to versioned release directory 7. Returns site URL **Flags**: - `--name ` — Custom subdomain (3-30 chars, `[a-z0-9-]`, no `--`, no leading/trailing `-`) **Behavior**: - No `--name`: server auto-generates name like `swift-cloud` (adjective-noun pattern) - Same name: updates existing site (new version, keeps last 5 releases) - Someone else's name: returns error (ownership check) - New name when at 20 sites: returns error **Output**: `https://.marshub.uz` ### `marshub ls` List all deployed sites. Shows table with columns: name, status, URL, size, date. ### `marshub status ` Change site status. Valid values: `draft`, `preview`, `alpha`, `beta`, `live`. ### `marshub rm ` Delete a site permanently. Prompts for confirmation (y/n). Removes all files and database entry. ### `marshub whoami` Display account information: - Username and Telegram ID - Number of sites - Storage used / limit (1 GB) - API key expiration date ### `marshub init` Add a MarshHub deploy-instructions block to the project's agent docs, so AI agents in that project know how to deploy. Target file: first existing of `AGENTS.md`, `CLAUDE.md`, `.cursorrules` (creates `AGENTS.md` if none exist), or `--file `. Idempotent: the block is wrapped in `` / `` markers and replaced on re-run. ### `marshub mcp` Run an MCP (Model Context Protocol) server over stdio. Exposes five tools: | Tool | Arguments | Description | |------|-----------|-------------| | `marshub_deploy` | `directory?`, `name?` | Pack and deploy a directory, returns live URL | | `marshub_list_sites` | — | List user's sites | | `marshub_delete_site` | `name` | Delete a site | | `marshub_set_status` | `name`, `status` | Set draft/preview/alpha/beta/live | | `marshub_whoami` | — | Account info | Setup in Claude Code: `claude mcp add marshub -- npx -y marshub mcp` Auth comes from `~/.marshubrc` (run `marshub login` once beforehand). ### `marshub help` Show list of available commands. ### Global Flag: `--json` Add `--json` to any command for machine-readable JSON output. Suppresses spinners, ANSI colors, and human-readable formatting. In JSON mode, `marshub rm` skips the confirmation prompt. **Auto-detection:** JSON mode turns on automatically when the CLI detects it is run by an AI agent — env vars `CLAUDECODE` (Claude Code), `CURSOR_AGENT` (Cursor) or `MARSHUB_AGENT` (manual override). `marshub login` in JSON mode emits `{"event":"auth_required","url":...}` then `{"event":"confirmed"}`. **Examples:** ```bash marshub ls --json # [{"name":"my-site","url":"https://my-site.marshub.uz","status":"live","size":123456,"updated_at":"..."}] marshub deploy --name my-site --json # {"name":"my-site","url":"https://my-site.marshub.uz","size":123456,"version":3} marshub whoami --json # {"username":"john","telegram_id":123,"sites_count":5,"storage_used":52428800,...} ``` Errors: `{"error":"","message":""}` with exit code 1. Error codes: `not_authenticated`, `token_expired`, `too_large`, `storage_exceeded`, `connection_failed`, `pack_failed`, `api_error`. --- ## Constraints | Parameter | Value | |-----------|-------| | Max upload size | 50 MB per deploy | | Storage per user | 1 GB total | | Max files per site | 5,000 | | Max sites per user | 20 | | API key lifetime | 90 days | | Deploy rate limit | 10 per minute per user | | Auth rate limit | 10 per minute per IP | | Site name length | 3-30 characters | | Site name pattern | `[a-z0-9-]`, no `--`, no leading/trailing `-` | ### Blocked File Extensions Server rejects files with these extensions during deploy: `.php`, `.sh`, `.bash`, `.cgi`, `.pl`, `.py`, `.rb`, `.exe`, `.bat`, `.cmd`, `.ps1`, `.jar`, `.war`, `.jsp`, `.asp`, `.aspx` ### Security - Path traversal protection during tar extraction - Symlinks are blocked (detected and rejected) - No server-side execution — static files only - API keys are 32-char random strings prefixed with `mh_` - All requests logged to SQLite (method, path, status, IP, user agent, duration) --- ## REST API **Base URL**: `https://marshub.uz/api` ### Public Endpoints (no auth) #### `GET /api/health` Health check. **Response**: `{"status":"ok"}` #### `GET /api/gallery` List all sites (public gallery). **Response**: Array of site objects with name, url, status, size, updated_at, screenshot_url, owner username. #### `GET /api/check-domain?domain=` Validate if a subdomain exists. Used internally by Caddy for on-demand TLS certificate issuance. #### `GET /api/hit/:site` Increment view counter for a site. #### `GET /api/views/:site` Get view count for a specific site. #### `GET /api/views` Get view counts for all sites. #### `GET /api/screenshot/:site` Get screenshot image of a site (auto-generated on deploy). ### Authenticated Endpoints All require `Authorization: Bearer ` header. #### `POST /api/deploy` Deploy a static site. **Content-Type**: `multipart/form-data` **Fields**: - `file` (required): tar.gz archive of site files - `name` (optional): desired subdomain name **Headers** (alternative): - `X-Site-Name`: site name (same as `name` form field) **Response** (201): ```json { "name": "my-site", "url": "https://my-site.marshub.uz", "size": 1234567, "version": 3 } ``` **Errors**: - 400: No file uploaded - 403: Max 20 sites / name taken by another user - 413: Upload > 50 MB - 429: Rate limit exceeded - 507: Storage quota exceeded (1 GB) #### `GET /api/sites` List user's sites. **Response**: Array of `{name, url, status, size, created_at, updated_at}` #### `GET /api/sites/:name/files` List files in a deployed site with sizes. **Response**: Array of `{path, size}` #### `DELETE /api/sites/:name` Delete a site. Must be owned by authenticated user. #### `PATCH /api/sites/:name/status` Update site status. **Body**: `{"status": "draft|preview|alpha|beta|live"}` #### `GET /api/versions/:name` List deploy versions for a site. **Response**: Array of version objects with number, date, size. #### `POST /api/rollback/:name` Rollback to previous version. Switches the `current` symlink to the previous release. #### `GET /api/me` Get authenticated user profile. **Response**: ```json { "username": "john", "telegram_id": 123456, "sites_count": 5, "storage_used": 52428800, "storage_limit": 1073741824, "api_key_expires_at": "2026-05-15T00:00:00.000Z", "created_at": "2026-02-13T10:00:00.000Z" } ``` ### Auth Endpoints #### `POST /api/auth/session` Create a login session for CLI auth. **Body**: `{"session_id": ""}` #### `GET /api/auth/poll?session=` Poll for login confirmation. **Response**: - Pending: `{"status": "pending"}` - Confirmed: `{"status": "confirmed", "api_key": "mh_..."}` - Expired: `{"status": "expired"}` #### `POST /api/auth/token` Exchange 6-char token for API key (legacy flow). **Body**: `{"token": "abc123"}` **Response**: `{"api_key": "mh_..."}` --- ## Deploy Internals When `marshub deploy --name my-site` runs: 1. CLI packs current directory into `tar.gz` (excluding node_modules, .git, .env, .DS_Store, __pycache__) 2. Uploads to `POST /api/deploy` as multipart form 3. Server checks: auth valid, user owns name (or name is new), under 20 sites, under 50 MB, under 1 GB total 4. Extracts archive to temp directory 5. Scans for blocked extensions (.php, .sh, etc.) — rejects if found 6. Scans for symlinks — rejects if found 7. Counts files — rejects if > 5,000 8. Moves to `sites//releases//` 9. Updates `sites//current` symlink → `releases/` 10. Cleans up old releases (keeps last 5) 11. Takes screenshot (async, via headless browser) 12. Returns `{"name", "url", "size", "version"}` The site is immediately accessible at `https://.marshub.uz` because: - Caddy has a wildcard handler for `*.marshub.uz` - On first request, Caddy calls `/api/check-domain` to verify the subdomain exists - If valid, Caddy issues a TLS certificate via Let's Encrypt (on-demand TLS) - Caddy serves files from the `current` symlink directory --- ## Config File CLI config at `~/.marshubrc`: ```json { "token": "mh_abc123...", "api": "https://marshub.uz" } ``` --- ## Error Messages All CLI output is in Russian. Common errors: | Russian Message | English Meaning | Solution | |----------------|-----------------|----------| | Сначала авторизуйся: marshub login | Not authenticated | Run `marshub login` | | Токен недействителен | API key expired | Run `marshub login` | | Не удалось подключиться к серверу | Server unreachable | Check network / server status | | Проект слишком большой (макс 50MB) | Upload too large | Remove large files | | Превышен лимит хранилища (1GB) | Storage quota exceeded | Delete sites with `marshub rm` | | Максимум 20 сайтов | Site limit reached | Delete unused sites | | Слишком много деплоев. Подожди минуту. | Rate limited | Wait 60 seconds | --- ## Database Schema SQLite with WAL mode. **users**: id, telegram_id (unique), api_key (unique), username, created_at, storage_used, api_key_created_at **sites**: id, name (unique), user_id (FK), size_bytes, status, created_at, updated_at **auth_tokens**: token (PK), telegram_id, created_at, used **auth_sessions**: session_id (PK), telegram_id, api_key, created_at, status (pending/confirmed/expired) **request_logs**: id, timestamp, method, path, status, user_id, ip, user_agent, duration_ms --- ## Naming **Auto-generated names** follow the pattern `adjective-noun`: - Adjectives: bright, swift, calm, bold, cool, fast, wise, keen, warm, soft, brave, fresh, kind, neat, pure, rare, true, wild, vivid, clear, crisp, prime, royal, smart, grand, noble, super, ultra, cyber, pixel, lunar, solar, turbo, micro, mega, alpha, beta, delta, sigma, orbit, spark, flame, frost, storm, cloud, wave, light, shadow, cosmic, golden, silent, rapid, steady, lucky, happy - Nouns: star, fox, river, moon, hawk, wolf, pine, reef, peak, lake, dawn, spark, bloom, grove, cliff, shore, breeze, flame, stone, ridge, forge, haven, quest, vault, tower, bridge, craft, pulse, nexus, prism, orbit, comet, arrow, blade, storm, ocean, atlas, pilot, scout, eagle, raven, tiger, cobra, lynx, panda, phoenix, dragon, titan, hero, sage, crest, flare, ember, haze, drift **Custom names** must pass validation: 3-30 chars, `[a-z0-9-]`, no double hyphens, no leading/trailing hyphens. --- ## Common AI Agent Patterns ### Deploy a project ```bash cd /path/to/project marshub deploy --name my-project ``` ### Deploy a subdirectory (e.g., build output) ```bash cd /path/to/project/dist marshub deploy --name my-app ``` ### Check auth status ```bash marshub whoami # Error "Сначала авторизуйся" means not logged in ``` ### List sites then delete one ```bash marshub ls marshub rm old-site # answer 'y' to confirm ``` ### Update and redeploy ```bash # Edit files, then: marshub deploy --name existing-site # Site updates in-place, old version kept for rollback ``` ### Rollback via API (not available in CLI) ```bash curl -X POST https://marshub.uz/api/rollback/my-site \ -H "Authorization: Bearer mh_..." ```