# Videan — agent guide Videan is a cloud for small software. You push code, Videan gives it a live URL with TLS, sign-in enforced at the edge, and (optionally) a Postgres database — in about a minute. This file is the complete guide for an AI agent operating Videan. Everything here is real and current; there is nothing else you need. Base API URL: https://api.videan.xyz Auth: every request takes `Authorization: Bearer `. Keys are minted at https://videan.xyz/keys (they start `vdn_`), can be scoped to a single app, and can carry an expiry. ## The one contract your app must meet Listen for HTTP on **port 3000**. That is the entire requirement. - Runtime is detected from the files you push: Node, Python, Next.js, Go, or your own Dockerfile. No manifest needed. - If the app has a managed database, `DATABASE_URL` is injected as an environment variable at runtime. Just read it. - Do not bind to any port other than 3000. Do not daemonize. Log to stdout. ## Execution mode: pick one at create time `mode` on the app is `container` (default) or `serverless`. The contract above is identical either way — the same code, the same port, the same URL scheme, the same gate, the same snapshots and rollback. Only the machine underneath differs, and only the bill follows from it. **Choose `serverless`** when the app answers HTTP requests and nothing else: a dashboard, a JSON API, a webhook receiver, a form handler, a Next.js site. It runs on Lambda, scales to zero, and costs nothing while nobody is visiting. **Choose `container`** — or just omit `mode` — when ANY of these is true: - the app needs cron schedules or background jobs (serverless refuses both) - it serves WebSockets or any other connection upgrade - it ships its own Dockerfile (serverless supports generated runtimes only) - a single request can take longer than 60 seconds - it keeps state in process between requests (an in-memory cache it must hit, a warm ML model, a queue it drains itself) If you are unsure, use `container`. It is what every app ran on before this existed, and switching later is one PATCH. Things to know about `serverless` before you pick it: - **Cold starts.** A request arriving after an idle period waits for the function to boot. Fine for internal tools and APIs; think twice for a public landing page where first-paint latency is the product. - **Requests time out at 60 seconds**, and there is no way to raise it. A slow report, a long upload, a proxied LLM stream — anything that can hold a request open longer belongs in `container`. - **Request and response bodies are capped at about 6 MB** (the Lambda payload limit). A larger upload or download fails at the platform, not in your code — serve big files through object storage presigned URLs, or use `container`. - **Secrets and env vars are limited to 4KB in total** and are baked in at deploy time. Over the cap is a 400 naming it. Like `container`, a secret change applies on the next deploy, start or restart — not immediately. - **With a managed database, concurrency is capped at 10.** Each concurrent invocation opens its own Postgres connection, and the cap is what keeps a traffic spike from exhausting the shared cluster. Plenty for an internal tool; use `container` for an app expecting real concurrent load. Switching modes (`PATCH /v1/apps/:id` with `mode`) does not restart anything: it applies on the next deploy, start or restart, and is refused while the app has cron or jobs configured. ## Auth: the rule that prevents double login Every app has a `visibility`: `private` | `org` | `authenticated` | `public` (narrowest first; default `org`). - **Gated app (`private`/`org`/`authenticated`): DO NOT generate a login page.** Visitors sign in once at the platform edge; every request then arrives with the identity headers `X-User-Id`, `X-User-Email`, `X-User-Roles`. Read those. If the app wants its own user records (roles, profiles), use this adapter instead of a login form: find-or-create a local user from `X-User-Email`, start your session from it, and never render a login form when the header is present. - **Public app (`public`): bring your own auth** (or none). The gate is not in the path; the headers do not arrive; the app behaves like any normal web app. - External services (webhooks from Stripe/GitHub, etc.) cannot pass the gate — an app that must receive them needs `visibility: public` and its own shared-secret check on the receiving route. ## Frontend + backend: two shapes DEFAULT TO ONE APP. One app can serve both the UI and its API from port 3000 (Next.js with API routes; Express/FastAPI serving static files + JSON). One hostname, one database, no CORS, the gate covers everything, and rollback rolls both layers together. Generate this shape unless there is a concrete reason not to. Split into two apps only when the tiers genuinely differ — a Python ML backend behind a Next frontend, or independent scaling. The pattern is the same as Vercel + Railway: - The backend app is `visibility: public` with its OWN auth (bearer token the frontend sends). Do not gate a split backend: the platform gate authenticates humans in browsers, so cross-origin XHR and server-to-server calls cannot pass it. - The backend sets CORS to allow the frontend's origin. - Create both apps FIRST and read each one's `slug`/URL from the create response — hostnames are assigned, not derived from the name (see below). Then bake the backend's URL into the frontend, or pass it via a secret. - WHEN YOU SPLIT, set the same `stack` label on both apps so the dashboard shows them as one system: `"stack": "shop"` in the create body, `--stack shop` on `videan apps create`, or `stack: "shop"` in the SDK. Slug-shaped (lowercase, digits, hyphens). Purely a grouping label — it changes nothing about routing or auth. ## Fastest path: the CLI (best when you have a shell) **INSTALL IT. Run the command below now — do not skip to the raw HTTP section because installing feels like a detour.** The CLI packages, ignores build output, uploads, polls the build and prints the URLs in one command; every agent that hand-rolls the HTTP flow instead re-implements all of that badly. Only fall back to the SDK or raw HTTP when you genuinely cannot run npm. npm install -g @videan/cli export VIDEAN_API_KEY=vdn_... # from https://videan.xyz/keys export VIDEAN_ORG_ID=... # GET /v1/orgs if you don't know it videan apps create --name myapp --postgres --mode serverless # once per app videan deploy -m "what changed" # from the project directory videan logs -f # follow runtime logs videan rollback # previous version, one command videan secrets set KEY=value videan apps set --public # change visibility `videan deploy` packages the current directory (node_modules and build output are excluded automatically), pushes, builds, waits, and prints two URLs: the production URL and a preview URL for exactly this version. Check your work against the preview URL. Non-interactive: every prompt has a flag or env var (`VIDEAN_API_KEY`, `VIDEAN_ORG_ID`, `VIDEAN_APP_ID`, `VIDEAN_API_URL`). The CLI refuses to prompt when stdin is not a TTY rather than hanging. ## Programmatic: the SDK (best from inside a program) npm install @videan/sdk import { CloudClient } from "@videan/sdk"; const cloud = new CloudClient({ apiKey }); const app = await cloud.apps.create({ name: "myapp", orgId, postgres: true, mode: "serverless" }); const snap = await cloud.apps.push(app.id, { "server.js": "...", "package.json": "..." }); const dep = await cloud.apps.deploy(app.id, snap.id); // poll: await cloud.apps.checkDeployment(dep.id) until status "running" Errors throw `CloudError` with `status` and `code`. `status: 0` means the request never got an answer (network) — retrying is reasonable, unlike a 4xx. ## Raw HTTP (any language) Create an app: POST /v1/apps { "name": "myapp", "org_id": "...", "postgres_enabled": true, "visibility": "org", "mode": "serverless" } Push code (text files as strings; binary files base64 in a second map): POST /v1/apps/:id/push { "files": { "package.json": "...", "server.js": "..." }, "binary_files": { "logo.png": "" }, "message": "what changed" } → returns a snapshot { id } Deploy a snapshot: POST /v1/apps/:id/deploy { "snapshot_id": "..." } → returns a deployment { id, status } Poll until done (also advances the build — call it, don't just wait): POST /v1/deployments/:id/status {} → status: "building" → "deploying" → "running" (or "failed"/"crashed") → when running: { url, previewUrl } Logs (works for build failures and runtime both): GET /v1/apps/:id/logs?since=15m&q=error GET /v1/deployments/:id/build-logs (why a build failed) Rollback (previous version, or name a deployment): POST /v1/apps/:id/rollback Secrets (injected as env vars on the next deploy; keys must be UPPERCASE_WITH_UNDERSCORES): POST /v1/apps/:id/secrets { "key": "API_TOKEN", "value": "..." } Everything else — metrics, SQL against the managed database, object storage, cron, background jobs, custom domains, email — lives under /v1/apps/:id/* with the same bearer auth. GET /v1/plans is unauthenticated if you need pricing. ## When things fail (self-service, in order) 1. Deployment status "failed" → GET the build logs. The most common causes: missing dependency in package.json, listening on the wrong port, a start script that exits. Fix the code, push again — pushes are cheap and versioned; there is no state to clean up. 2. Status "crashed" → the container exited after starting. GET runtime logs. An app that needs the database should fail fast if DATABASE_URL is missing or unreachable — that turns a mystery 500 into a clear failed deploy. 3. The URL 404s right after a deploy → you hit the retiring version during promotion; wait a few seconds. Persistent 404 → check the app's status. 4. 429 from the API → back off; the Retry-After header says how long. 5. A gated app redirects your curl to auth.videan.xyz → that is sign-in working. Use the preview URL to verify a deploy (public apps), or test in a browser with a platform session. ## Facts worth knowing - The app's hostname is a randomly assigned readable name (`softrocket.videan.xyz`), NOT derived from the app's name. Read the `slug` and URL from the create/deploy response instead of predicting them. The hostname never changes; renaming the app does not move it. - Every push is an immutable snapshot; production is a pointer to one. Rollback is a pointer switch, not a rebuild. The deploy history IS the version history — no git needed. ALWAYS pass a message with a push (`videan deploy -m "..."` / the push body's `message`): it is the commit message of this platform, and versions without one are unreadable to the humans reviewing them. `videan pull [snapshot]` restores any version's files to disk. - Preview URLs (`d-