# Light Cloud Docs > Documentation for the Light Cloud deployment platform. Deploy web apps from Git: static sites, containers, and managed Postgres, with a preview for every branch and deploys you can run from Claude or GitHub Copilot. # Introduction Light Cloud is a deployment platform for web apps. Connect a GitHub, GitLab or Bitbucket repository, push, and get a live URL. It runs static sites, containers, and managed databases on one platform. ## What you can deploy - **Static sites** served from a global CDN. React, Vue, Angular, Svelte, SolidJS, Qwik, Gatsby, plain HTML, and Next.js static export. - **Containers** on Cloud Run. Next.js, Nuxt, SvelteKit, Remix, Django, Rails, Laravel, Spring Boot - any Node, Python, Go, Java, Ruby, PHP, or .NET app, or your own Dockerfile. See [Frameworks](/platform/frameworks) for the full list. - **Managed PostgreSQL or MySQL** provisioned next to your app. See [Databases](/platform/databases). ## How it works 1. Connect a GitHub, GitLab or Bitbucket repository. 2. Light Cloud detects the framework and the build. 3. Every push builds the commit and deploys it. 4. Static assets go to the CDN, containers run on Cloud Run. 5. You get a live URL, plus logs and metrics. The full pipeline, step by step, is on [How it works](/getting-started/how-it-works). ## Three ways to deploy - **From GitHub, GitLab or Bitbucket.** Connect a repo and every push ships. See [Deployments](/platform/deployments) and [Git providers](/platform/deployments/git-providers). - **From your AI.** Deploy from Claude with the MCP server, or from GitHub Copilot with the VS Code extension. See [Deploy with AI](/deploy-with-ai/overview). - **From a local folder.** Upload a project without a repository. See [Deploy from an upload](/create/upload). ## The pieces | Piece | What it is | Docs | | --- | --- | --- | | Application | One deployed project, linked to one repository | [Deployments](/platform/deployments) | | Environment | One running copy of an application, one per branch | [Preview environments](/platform/preview-environments) | | Deployment | One build and release of one commit | [Builds](/platform/deployments/builds) | | Database | Managed PostgreSQL or MySQL next to your apps | [Databases](/platform/databases) | | Domain | Your own hostname on any environment | [Custom domains](/platform/custom-domains) | ## Where to next - [Quickstart](/getting-started/quickstart): Take a GitHub, GitLab or Bitbucket repository to a live URL in a few minutes. - [How it works](/getting-started/how-it-works): The build and deploy pipeline behind every push. - [Creating resources](/create/overview): Every field on the Create page, explained. - [Pricing](/billing/pricing): Five plans with usage included, from Free with unlimited static sites, and the rates every plan meters at. --- # Quickstart This guide takes a GitHub, GitLab or Bitbucket repository to a live URL. It works for any framework Light Cloud [detects](/platform/frameworks#how-detection-works) - a static frontend, a server-rendered app, or an API. ## Before you start - A GitHub, GitLab or Bitbucket account with a repository you can deploy. Any framework from the [supported list](/platform/frameworks) works, and a repository with a Dockerfile always works. - Nothing to install. Everything in this guide happens in the browser. No repository at hand? Start from a [template](/create/templates) instead, or [upload a folder](/create/upload). ## 1. Sign in Open the [console](https://console.light-cloud.com) and sign in with GitHub, GitLab or Bitbucket. On GitHub, Light Cloud installs a GitHub App so it can read the repositories you choose and build them on push - you pick which repositories the app can see, and can change the selection later in GitHub. On GitLab and Bitbucket, it connects through your user and can deploy any project or repository you have access to. Details for all three: [Git providers](/platform/deployments/git-providers). ## 2. Create an application Go to **Create** and pick a repository and a branch. Light Cloud reads the repo and detects the framework, the build command, and the output directory. The badge above the form says what was found and from which files. Override anything that looks wrong - every field is editable, and every field is documented under [Creating resources](/create/overview). ## 3. Deploy Confirm the settings and deploy. The build runs and the [logs stream](/platform/observability#build-logs) while it does. When it finishes you get a URL: ``` {branch}-{app}-{org}.light-cloud.io ``` A static build is served from the CDN. A backend build runs as a container on Cloud Run and [scales to zero](/create/scaling#min-instances) when idle. ## 4. Push to ship again Every push to the connected branch builds and deploys on its own. Open a pull request and you get a [preview environment](/platform/preview-environments) with its own URL to share with reviewers. > [!TIP] > The first deploy is the slowest one - later builds reuse caches. If the first build fails, the failing lines are pulled out of the build output and shown on the deploy record. See [Builds](/platform/deployments/builds#when-a-build-fails). ## Prefer your terminal or editor? Deploy from Claude or GitHub Copilot instead of the dashboard. See [Deploy with AI](/deploy-with-ai/overview). ## Next steps - [Environment variables](/platform/environment-variables): Configuration and secrets, per app and per environment. - [Custom domains](/platform/custom-domains): Serve the app on a domain you own, with automatic certificates. - [Databases](/platform/databases): Provision managed PostgreSQL or MySQL next to the app. - [Scaling](/create/scaling): Instances, concurrency, and what scale to zero means for cost. --- # How it works This page is the mental model for the whole platform: what happens between a `git push` and a running app, and what the moving parts are called. ## The pipeline When you push to a connected branch: 1. **Webhook.** GitHub, GitLab or Bitbucket sends a signed webhook; Light Cloud matches it to the application and the environment for that branch. Apps with a [root directory](/create/application-settings#root-directory) only rebuild when the push touches that folder. 2. **Build.** The commit is built with Cloud Build. If the repo needs a Dockerfile and has none, one is generated for your framework. The details are on [Builds](/platform/deployments/builds). 3. **Store.** A static build is uploaded to object storage. A container build is pushed to Artifact Registry as an image. 4. **Release.** Static assets go live on the global CDN. A container image is deployed to Cloud Run, which routes traffic to the new revision. 5. **Record.** The deploy is recorded with its commit, author, duration, status, and logs. See [Observability](/platform/observability). Every step streams into the deploy's build log, so a failing deploy tells you which step broke and why. ## Applications and environments An **application** is linked to one repository. It has one or more **environments**, one per connected branch. Each environment has its own URL, build settings, and [environment variables](/platform/environment-variables). - One environment is **production** - protected from cleanup, deployed from the production branch. See [Deployments](/platform/deployments#the-production-environment). - The rest are **previews** you can create and remove freely. See [Preview environments](/platform/preview-environments). One commit built and released for one environment is a **deployment**. Each environment keeps its last 20 deployments in its history. ## URLs Every deployed environment gets a URL built from the branch, the app, and your organization: ``` {branch}-{app}-{org}.light-cloud.io ``` Attach a domain you own on top of it - see [Custom domains](/platform/custom-domains). ## Where it runs Light Cloud runs on Google Cloud and a Cloudflare CDN: | Workload | Runs on | | --- | --- | | Container apps | Cloud Run | | Image builds | Cloud Build, stored in Artifact Registry | | Static sites | Object storage behind a global CDN | | Databases | Cloud SQL | You never touch these services directly - Light Cloud provisions and operates them, and [meters them against your plan's included usage](/billing/pricing). ## What this model buys you - **Scale to zero.** Container apps with no traffic stop billing. See [Scaling](/create/scaling#min-instances). - **A preview per branch.** Isolated environments with their own URLs and variables, cleaned up when the branch is deleted. - **Reproducible releases.** Every deploy is one commit, built the same way every time. Rolling back is a `git revert` away - see [Rollbacks](/platform/deployments/rollbacks). ## Related - [Builds](/platform/deployments/builds): What Cloud Build runs, how Dockerfiles are generated, and why builds fail. - [Preview environments](/platform/preview-environments): The environment-per-branch lifecycle in detail. - [Frameworks](/platform/frameworks): What detection reads and every supported framework. - [Glossary](/reference/glossary): Every platform term, defined in a sentence or two. --- # FAQ Short answers, with links to the full story. ## Deploying ### What can I deploy? Static frontends, server-rendered apps, APIs, and anything with a Dockerfile. The full list is on [Frameworks](/platform/frameworks). Managed [PostgreSQL and MySQL databases](/platform/databases) run next to your apps. ### Do I need a Dockerfile? No. If a framework needs a container and the repo has no Dockerfile, one is generated for it. If you commit a Dockerfile, it is used exactly as written. See [Builds](/platform/deployments/builds#dockerfiles-generated-or-your-own). ### Does it work with monorepos? Yes. Set the [root directory](/create/application-settings#root-directory) to the folder your app lives in. Detection, builds, and webhook triggers all honour it - a push that does not touch that folder does not rebuild the app. ### Can I deploy without a Git repository? Yes - upload a folder or a `.zip` from the console, from Claude, or from VS Code. See [Deploy from an upload](/create/upload). ### How do I roll back a bad deploy? Revert the commit in Git and push - the pipeline ships it like any other change. **Redeploy** in the console re-ships the branch tip, which helps after settings changes. See [Rollbacks](/platform/deployments/rollbacks). ## Cost ### What do I get for free? Unlimited static sites, three server apps and a preview URL for every branch, each on a light-cloud.io address, with $1 of usage a month. No card. A typical site uses about a cent of it, and Free has no end date. See [Pricing](/billing/pricing). ### What's the catch? Free pages carry a small "by Light Cloud" link, there is no custom domain, and server apps pause if you use the whole $1 in a month. Lite removes all three for $5. Free never bills you. It pauses: when the $1 is used up, server apps and deploys pause until the next billing cycle or an upgrade, and your static sites stay up. See [Billing](/billing/billing#the-free-plan). ### What does an idle app cost? A container app with [min instances](/create/scaling#min-instances) at 0 (the default) scales to zero when traffic stops and counts nothing while asleep. Static sites run no instances: they count their build minutes and storage, and their bandwidth is included. See [Billing](/billing/billing#scale-to-zero). ### What happens if I use more than my plan includes? On a paid plan the extra usage is added to your next invoice at the same rates - nothing pauses and nothing is charged mid-month, unless you set a [usage limit](/billing/billing#usage-limit). You get an email at 80% of the included usage and another when it is used up, and you can add one more alert at a dollar amount of your choosing. See [Billing](/billing/billing#extra-usage). ### How do I upgrade? From Billing in the console, or from the prompt that appears when you reach for a paid feature. One Stripe Checkout takes your card and the first payment together, and paused server apps resume within minutes; with a card already saved it is one click. Paid plans can also be paid yearly, with two months free. See [Billing](/billing/billing#upgrading). ## Environments ### How do preview environments get created? Push a branch and it gets its own environment and URL, rebuilt on every push and removed when the branch is deleted. See [Preview environments](/platform/preview-environments). ### Can previews have their own configuration? Yes. An environment starts from the application's [environment variables](/platform/environment-variables) and can override any of them - point a preview at a test database while production keeps the real one. ### Can I password-protect an environment? Yes - give it a visitor password in its settings. See [Preview environments](/platform/preview-environments#protect-a-preview). ## Domains and databases ### How do custom domains work? Custom domains come with every paid plan, from Lite at $5 a month. Add the domain on the environment's Domain tab, create the DNS records shown, and the certificate is issued and renewed for you. See [Custom domains](/platform/custom-domains). ### Which databases are available? Managed PostgreSQL 15 and MySQL 8.0, from a $4 shared tier to dedicated instances. Databases start on Lite, which allows one shared database; Starter and up can create more. See [Databases](/platform/databases) and [Connect an app](/platform/databases/connect). ### Are databases backed up? Daily automatic backups for every database, plus point-in-time recovery for PostgreSQL. See [Backups](/platform/databases/backups). ## Something else? - The [Glossary](/reference/glossary) defines every platform term. - [Limits](/reference/limits) collects every number in one table. - Ask from your editor: the [MCP server](/deploy-with-ai/mcp-server) can answer "why did my deploy fail?" against your real logs. --- # Creating resources Everything a workspace can run starts on the **Create** page (`/new`): applications from a Git repository, a `.zip` upload or a starter template, and managed databases. ## The four sources - [Repository](/platform/deployments): Connect GitHub, GitLab or Bitbucket and deploy on every push. - [Upload](/create/upload): Deploy a local folder or .zip without a repository. - [Template](/create/templates): Start from a working starter, copied into a repo you own. - [Database](/create/database-settings): Provision managed PostgreSQL or MySQL. ## How the page works 1. **Pick what you're creating.** The four tiles at the top - Repository, Upload, Template, Database - stay on screen the whole time, so switching is always one click. 2. **Pick a source.** A repository from your connected GitHub, GitLab or Bitbucket account, an archive, a starter, or a database engine. 3. **Confirm the settings.** For repositories, Light Cloud reads the code first - manifests, lockfiles, framework config - and fills the form with what it found. Everything on the confirm step is editable before you deploy. > [!NOTE] > Detection fills settings; it never locks them. If you change a value by hand and a later re-detection disagrees, the page asks before replacing your edit. ## Where each setting is explained - [Application settings](/create/application-settings): Name, root directory, framework, size, region. - [Build settings](/create/build-settings): Port, install, build and output configuration. - [Scaling](/create/scaling): Instances, concurrency, CPU target. - [Git automation](/create/git-automation): Deploy on push, preview environments per branch, cleanup. - [Database settings](/create/database-settings): Engine, shared vs dedicated, storage, credentials, seeding. Every field on the Create page links to its section in these docs from the small help icon next to its label. ## After the first deploy The created application appears on your dashboard with its [deploy history](/platform/deployments#deploy-history), [logs and metrics](/platform/observability), and settings - everything chosen at create time can be changed later, per application and per [environment](/platform/preview-environments). --- # Application settings The first card on the Create page describes what the application *is*: what it's called, where its code lives, what framework it runs, and where it runs. Detection pre-fills most of it for repository sources; everything stays editable. ## Name The display name for the app across your dashboard. It also seeds suggested defaults - the repository name for a starter copy, the database name for a stack - which you can change independently. Lowercase letters, numbers and dashes travel best, because the name feeds generated identifiers and the app's `*.light-cloud.io` URL. **Example:** `my-shop` ## Root directory For monorepos: the folder Light Cloud builds from. Blank means the repository root. Setting it re-runs detection against that folder, so the framework, build command and output directory describe the app you're deploying rather than the repository as a whole. The folder button next to the field browses the repository so you don't have to guess the path. Webhook builds also honour it - a push that doesn't touch the root directory doesn't rebuild the app. **Example:** `apps/web` in a repository laid out as: ``` apps/ web/ <- deploy this api/ packages/ ui/ ``` > [!TIP] > Deploying several apps from one repository? Create one application per folder, each with its own root directory. Each app then builds only when its folder changes. ## Framework What the app is built with. This decides the whole build recipe: static frameworks are built and served from the edge CDN; server frameworks run as containers. For repositories the framework is detected from the code - the badge above the form says what was found and from which files. Change it if the guess is wrong; the [Frameworks](/platform/frameworks) page lists everything supported and [how detection works](/platform/frameworks#how-detection-works). ## Size The memory and CPU each running instance gets. Bigger sizes serve more load per instance and cost more per hour - the rates are on [Pricing](/billing/pricing#containers). | Size | vCPU | Memory | Notes | | --- | --- | --- | --- | | Nano | 1 | 256 MB | Always scales to zero; smallest and cheapest | | Micro | 1 | 512 MB | The default | | Small | 1 | 1 GB | | | Medium | 2 | 2 GB | | | Large | 4 | 4 GB | | Larger sizes, up to 8 vCPUs and 32 GB, are coming. Each size also caps [concurrency](/create/scaling#concurrency) - how many requests one instance serves at once. Two things can constrain the choice: - **Nano** can't keep CPU allocated while idle, so it always sleeps when traffic stops (scale to zero). - **Server-rendered frontends** (Next.js, Nuxt, SvelteKit, Remix, Astro in SSR mode) are pinned platform-wide to the Micro size - the edge serves your audience and the origin only renders cache misses, so a bigger origin buys nothing. See [the SSR policy](/create/scaling#the-ssr-policy). If the form moves your selection - because your plan doesn't include a size, or the SSR policy applies - it says so in the row it changed. ## Region The data centre the app runs in: | Region | Location | | --- | --- | | `europe-west1` | Belgium (Europe) | | `us-central1` | Iowa (US Central) | | `us-east1` | South Carolina (US East) | | `us-west1` | Oregon (US West) | Every request travels there and back, so pick the region closest to your users - and keep an app next to its [database](/create/database-settings#region). More regions are coming; see [Pricing](/billing/pricing#regions). Static sites don't have a region; they're served from the CDN everywhere. Server-rendered frontends are offered tier-1 regions only, since the edge sits in front of the origin anyway. ## GitHub repo (starter templates) When you create an app from a [starter template](/create/templates) with a GitHub, GitLab or Bitbucket account connected, the starter is copied into a new **private repository you own**, under the account shown in the row. The row holds a dropdown of the connected providers (each with the account it is connected as) when there is more than one, and the name is checked for availability under the chosen account as you type. Pushes to your copy deploy automatically - see [Git automation](/create/git-automation). Without any account connected, the app deploys read-only from the shared template; connect GitHub, GitLab or Bitbucket first to get an editable copy. See [Git providers](/platform/deployments/git-providers#starter-templates). **Example:** `my-shop` becomes `your-account/my-shop` ## Related - [Build settings](/create/build-settings): Port, install and build commands, output directory. - [Scaling](/create/scaling): Min and max instances, concurrency, CPU target. - [Frameworks](/platform/frameworks): Everything supported, and what detection reads. - [Pricing](/billing/pricing): Per-hour rates for every size and region. --- # Build settings These live under **Advanced** on the Create page because detection usually fills them correctly. Come here when a build needs something non-standard. What actually runs during a build is on [Builds](/platform/deployments/builds). Which fields you see depends on the framework's deployment type: | Deployment type | Fields used | | --- | --- | | **Static** - built once, served from the CDN | Install command, build command, output directory | | **Container** - runs a server | Build command (inside the image build), port | ## Port The port **inside the container** your server must listen on. Light Cloud routes all traffic to it. Detection reads the framework's default - a few examples: | Framework | Default port | | --- | --- | | Next.js, Nuxt, SvelteKit, Remix | `3000` | | Express, Go, Spring Boot, Laravel, ASP.NET | `8080` | | Django, Flask, FastAPI | `8000` | | AdonisJS | `3333` | If your server listens somewhere else, set it here - a mismatch shows up as a failed health check or an unreachable app after an otherwise green deploy. Most frameworks also respect the `PORT` [environment variable](/platform/environment-variables#build-time-and-runtime), which is set to this value at runtime. **Example:** `3000` ## Install command Runs first on every deploy to install dependencies. Left empty, it follows your lockfile: | Lockfile | Install | | --- | --- | | `pnpm-lock.yaml` | pnpm | | `yarn.lock` | yarn | | `package-lock.json` | npm (`npm ci`) | Override it to pass flags or run extra steps. **Example:** `npm ci --ignore-scripts` ## Build command Runs on every deploy to produce the app. For static sites it must fill the output directory. For containers it runs while the image is built (and your Dockerfile is used as-is if the repository has one - see [Deploy with a Dockerfile](/platform/frameworks/docker)). Chain steps with `&&` when a build needs more than one command. **Example:** `npm run build` ## Output directory Static sites only: the folder the build writes the finished site into. Its contents are exactly what the CDN serves - nothing outside it is deployed. Detection knows each framework's default: | Framework | Output directory | | --- | --- | | React, Vue, Svelte, SolidJS, Qwik, Astro (Vite-based) | `dist` | | Angular | `dist/browser` | | Next.js static export | `out` | | Gatsby | `public` | | Docusaurus | `build` | | Eleventy | `_site` | | Plain HTML | `.` (the root) | Set it when a config changes the destination. **Example:** `dist` ## Custom domain Serves the app on a domain you own as well as the generated `*.light-cloud.io` URL. The domain goes live once your DNS points at the platform - the full walkthrough, including DNS records and certificates, is on [Custom domains](/platform/custom-domains). **Example:** `app.example.com` ## Related - [Builds](/platform/deployments/builds): The pipeline these settings feed - steps, caching, failures. - [Frameworks](/platform/frameworks): Detection sources and defaults for every framework. - [Environment variables](/platform/environment-variables): Values available at build time and at runtime. - [Application settings](/create/application-settings): Name, root directory, framework, size, region. --- # Scaling Container apps scale horizontally: identical instances are added under load and removed when it passes. Four settings under **Advanced** shape that behaviour. Static sites have none of them - the CDN scales by itself. ## How autoscaling decides Two signals add instances: - **Concurrency.** Each instance serves at most its concurrency in parallel requests; when demand exceeds what the running instances can carry, more are started. - **CPU target.** When average CPU across instances crosses the target, an instance is added even if concurrency has headroom. Instances are removed as traffic falls, down to the minimum. Watch it happen on the **Active instances** chart - see [Metrics](/platform/observability#metrics). ## Min instances Instances kept running even with no traffic. - **0** (the default): the app sleeps when idle and [stops billing](/billing/billing#scale-to-zero). The first request after a quiet period pays a cold start - a few seconds while an instance boots. - **1 or more**: that many instances stay warm around the clock. No cold starts; each one is billed every hour, idle or not, at the [always-on rate](/billing/pricing#containers) - about $52 a month for a Micro. Always-on needs Starter or above. The Nano size can't stay warm - it always scales to zero. Pick a bigger size if you need a warm minimum. **Example:** `1` for an API whose first request must be fast. > [!TIP] > Scale to zero is what makes [preview environments](/platform/preview-environments) cheap: a preview nobody is looking at costs nothing. Keep previews at 0 and raise only production if cold starts hurt. ## Max instances The autoscaling ceiling. However hard traffic spikes, you never run - or pay for - more than this many instances at once. The default ceiling is 10; choices go up to 100, and your plan sets an upper bound for the ceiling itself. **Example:** `10` ## Concurrency How many requests a single instance handles at the same time (default `80`). Higher means fewer instances for the same traffic - cheaper, but each request shares the instance's memory and CPU with more neighbours. Lower gives each request more headroom; CPU-heavy or memory-hungry endpoints often want `10`-`20`. Each [size](/create/application-settings#size) has its own maximum: | Size | Max concurrency | | --- | --- | | Nano | 40 | | Micro | 80 | | Small | 100 | | Medium | 200 | | Large | 300 | **Example:** `80` for a typical I/O-bound API. ## CPU target A new instance is added once average CPU across instances crosses this percentage (default `80%`). A higher target runs instances hotter before scaling out - cheaper, with less headroom for sudden spikes. A lower target scales earlier and absorbs bursts more smoothly. **Example:** `70` for spiky traffic; `85` to squeeze cost on steady load. ## Cold starts An app at zero instances boots one on the first request: the image starts, your server begins listening on its [port](/create/build-settings#port), and the request is served. That is typically a few seconds, and it depends on your app - a lean server image cold-starts faster than one that loads heavy dependencies at startup. Ways to spend it down: - Set min instances to `1` on the environments where it matters (usually just production). - Keep startup work lazy: connect to the [database](/platform/databases/connect) on first use rather than at boot. - Keep the image lean - see [Deploy with a Dockerfile](/platform/frameworks/docker) for image-size advice. ## The SSR policy Server-rendered frontends - Next.js, Nuxt, SvelteKit, Remix, Astro in SSR mode - have their shape pinned platform-wide: **Micro size, scale 0-3, tier-1 region**, always behind the edge CDN. The edge serves your audience; the origin only renders cache misses, so scaling knobs are hidden for these apps and the concurrency/CPU settings don't apply. ## Related - [Application settings](/create/application-settings): Sizes, regions, and what pins them. - [Billing](/billing/billing): Instance-hours, scale to zero, and included usage. - [Observability](/platform/observability): The charts that show scaling working. - [Limits](/reference/limits): Every scaling number in one table. --- # Git automation Three toggles under **Advanced** decide what happens between a `git push` and a running deployment. All three default to on, and all can be changed later in the application's settings. ## Branch The branch production deploys from - the repository's default branch unless you pick another (GitLab and Bitbucket sources offer the full branch list at create time). **Example:** `main` ## Deploy on push Every push to the deployed branch builds and releases automatically, delivered by the GitHub, GitLab or Bitbucket webhook. For apps with a [root directory](/create/application-settings#root-directory), only pushes touching that folder rebuild (on Bitbucket every push rebuilds, as its payload lists no files). Turned off, nothing deploys by itself - you release manually with **Redeploy** from the dashboard, which suits apps where releases are scheduled rather than continuous. See [Rollbacks and redeploys](/platform/deployments/rollbacks). ## Deploy every branch Every branch other than the production one gets its own **preview environment**: an isolated deployment with its own URL - ``` {branch}-{app}-{org}.light-cloud.io ``` - rebuilt on every push to that branch. Previews are how a change is reviewed running, before it merges. They carry their own [environment variables](/platform/environment-variables) and can differ from production. Full story: [Preview environments](/platform/preview-environments). ## Remove stale environments When a branch is deleted - typically after its merge - its preview environment is deleted with it. Previews never pile up; nothing keeps billing for a branch that no longer exists. Turned off, previews out-live their branches until you delete them by hand. > [!NOTE] > Cleanup never touches the production environment, whatever happens to branches. See [Production is protected](/platform/preview-environments#production-is-protected). ## The full loop With all three on, the lifecycle of a change looks like this: 1. Push a branch. A preview environment appears at its own URL. 2. Push again while the review runs. The preview redeploys each time. 3. Merge. Production builds and releases the merge commit. 4. Delete the branch. The preview environment is removed. Nothing in that loop needs the dashboard - it is all driven by Git. ## Related - [Preview environments](/platform/preview-environments): Lifecycle, URLs, passwords, and cost of previews. - [Deployments](/platform/deployments): Connecting repositories and the production environment. - [Rollbacks](/platform/deployments/rollbacks): Redeploy, revert, and manual releases. --- # Database settings The Database tile creates a managed PostgreSQL or MySQL database. Defaults are chosen so the visible decisions stay small - size, storage, region - and the rest is generated safely. What databases are and how they connect to apps is covered in [Databases](/platform/databases); this page explains each field on the create form. ## Name The display name for the database across your dashboard. On dedicated instances it also seeds the suggested database name under Connection details - which stays editable. **Example:** `my-shop-db` ## Engine PostgreSQL or MySQL, both fully managed: | Engine | Version | Default port | | --- | --- | --- | | PostgreSQL | 15 | `5432` | | MySQL | 8.0 | `3306` | Pick whatever your framework's ORM expects - connection details come out in the right shape either way. See [Connect an app](/platform/databases/connect). ## Size The one decision with real consequences: - **Shared** (the default) runs your database on a pooled instance. It's ready in **seconds**, and it's the cheapest tier - right for development, previews and small production apps. See [The Shared tier](/platform/databases#the-shared-tier). - **Dedicated** sizes provision a private instance with reserved memory. Creation takes **10-20 minutes**, and the instance is yours alone - right for production loads that need predictable performance. The tiers and their prices are on [Databases](/platform/databases#tiers). If a size isn't offered, your plan doesn't include it. ## Storage How much disk the database gets. It can be **grown later without downtime, but never shrunk** - start small. | Tier | Storage choices | | --- | --- | | Shared | 1, 2, 5, or 10 GB (a storage quota on the shared instance) | | Dedicated | 10, 20, 50, or 100 GB of reserved SSD (250 and 500 GB coming) | **Example:** `10 GB` ## Region The data centre the database runs in. Keep it in the same region as the app that talks to it - every query crosses that distance. Shared databases are offered in the regions the Shared tier runs in (Belgium, `europe-west1`, today); if your selected region isn't one of them, the form says so and moves it. Dedicated instances are offered in every [platform region](/create/application-settings#region). ## Connection details Collapsed by default because every field has a safe generated value: - **Database name** - the initial database on the instance; what goes in your connection string. On the **shared** tier the name is generated opaque on purpose: co-tenants of a pooled instance can list database names, so yours must reveal nothing. Read it from the Credentials tab once ready. **Example (dedicated):** `shop_production` - **Admin user** - the instance's admin account: `postgres` for PostgreSQL, `root` for MySQL, unless you set another name (dedicated only). - **Password** - at least 12 characters, no spaces, quotes or backslashes. Left blank, a strong password is generated - read it any time from the Credentials tab. ## Seed from dump Shared tier only: pick a `.sql` or `.sql.gz` file and it's imported the moment the database is ready - the same format the Backup tab exports, so moving a database in is one create. If the import fails, the empty database survives and the import can be retried from its Backup tab. Dedicated instances take 10-20 minutes to provision, so their imports live on the Backup tab instead of the create form. See [Backups](/platform/databases/backups). **Example:** `backup.sql.gz` ## Related - [Databases](/platform/databases): Tiers, prices, the Shared tier, and high availability. - [Connect an app](/platform/databases/connect): Connection strings, TLS, and framework examples. - [Backups](/platform/databases/backups): Daily backups, point-in-time recovery, import and export. - [Pricing](/billing/pricing): Per-hour rates for every database tier. --- # Templates The Template tile on the [Create page](/create/overview) starts you from something that already works: a starter app, or a **stack** - an application plus a database, created and connected in one go. ## Your own copy With a GitHub, GitLab or Bitbucket account connected, the starter is copied into a new **private repository you own** - you choose the provider on the Create page when several are connected - and pushes to your copy deploy automatically - the same [Git automation](/create/git-automation) as any repository app. Without any connected, the app deploys read-only from the shared template. See [GitHub repo](/create/application-settings#github-repo-starter-templates) and [Git providers](/platform/deployments/git-providers#starter-templates). ## Stacks A stack provisions two resources and wires them together: 1. The application is created from the starter repository. 2. A [Shared-tier database](/platform/databases#the-shared-tier) is provisioned next to it - ready in seconds. 3. The connection details land in the app's [environment variables](/platform/environment-variables) before the first deploy, so the app boots connected. Stacks are created always-on (min instances `1`) so the app is warm from the first visit; you can change that in [Scaling](/create/scaling#min-instances) later. ### WordPress WordPress with a connected MySQL database: - **App:** WordPress (PHP container) from the `light-cloud-com/starter-wordpress` starter. - **Database:** MySQL, Shared tier, 1 GB. - **Wiring:** `WORDPRESS_DB_HOST`, `WORDPRESS_DB_NAME`, `WORDPRESS_DB_USER`, and `WORDPRESS_DB_PASSWORD` are filled in for you. Deploy, open the URL, and run the WordPress installer against a database that is already connected. ### Open SaaS The [Open SaaS](https://opensaas.sh) starter (built on Wasp) with a connected PostgreSQL database: - **App:** Wasp (Node.js container) from the `light-cloud-com/starter-opensaas` starter. - **Database:** PostgreSQL, Shared tier, 1 GB, wired in as `DATABASE_URL`. - **Generated for you:** a strong `JWT_SECRET` at create time, and `WASP_SERVER_URL` / `WASP_WEB_CLIENT_URL` derived from the app's public URL on every deploy. The starter has optional integrations that need keys from you. They are created as placeholder [environment variables](/platform/environment-variables) so the checklist is visible in the console: | Importance | Variables | Feature | | --- | --- | --- | | Required | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD` | Email - signup and password reset | | Recommended | `ADMIN_EMAILS` | Admin dashboard access | | Recommended | `STRIPE_API_KEY`, `STRIPE_WEBHOOK_SECRET`, `PAYMENTS_*_PLAN_ID` | Payments (Stripe) | | Optional | `OPENAI_API_KEY` | AI demo app | | Optional | `AWS_S3_*` | File uploads (S3) | | Optional | `LEMONSQUEEZY_*`, `POLAR_*` | Payments (alternatives) | | Optional | `PLAUSIBLE_*`, `GOOGLE_ANALYTICS_*` | Analytics | Set the required ones, redeploy, and the app is functional; fill in the rest as you enable each feature. ## After creating A template app is a normal application: it has [deploy history](/platform/deployments#deploy-history), [logs and metrics](/platform/observability), and every setting can be changed later. Edit code in your repository copy and push to ship. ## Related - [Creating resources](/create/overview): The Create page and its four sources. - [Databases](/platform/databases): What the Shared tier is and when to move off it. - [Environment variables](/platform/environment-variables): Editing the values a stack pre-filled. --- # Deploy from an upload Not everything lives in a repository. The Upload tile deploys a project from an archive of your local folder - same detection, same build, same live URL, no Git required. ## Three ways to upload - **The Create page.** Pick the Upload tile, drop a `.zip` of the project, and confirm the detected settings like any other app. - **Claude.** Open Claude in the project directory and ask it to deploy - the [MCP server](/deploy-with-ai/mcp-server#from-a-local-project) packages the folder, uploads it, and deploys. - **VS Code.** Run `/deploy` in Copilot Chat with the [Light Cloud extension](/deploy-with-ai/vscode-copilot) installed. When you deploy from Claude or VS Code, variables in your local `.env` file are read and [set on the app](/platform/environment-variables#from-a-env-file-automatically) for you. ## What's different from a repository app An uploaded app is a normal application - environments, logs, metrics, domains, variables all work - with one difference: there is no repository, so nothing deploys by itself. - **No webhooks.** [Git automation](/create/git-automation) doesn't apply; there is no branch to watch. - **Ship by uploading again.** Each new upload builds and deploys like a push would. From Claude or VS Code that is one command, because the [project config file](/deploy-with-ai/mcp-server#follow-up-commands) remembers which app the folder belongs to. - **No preview-per-branch.** You can still create extra [environments](/platform/preview-environments) and deploy uploads to them - it's just not automatic. > [!TIP] > Uploads are the fastest way to put a prototype online, but once a project is real, connect a repository - you get deploy-on-push, previews per branch, and a commit trail on every deploy for free. See [Deployments](/platform/deployments). ## What gets uploaded The project folder, minus what clearly isn't source: dependency directories like `node_modules` are ignored and restored by the [install command](/create/build-settings#install-command) during the build. Secrets should travel as [environment variables](/platform/environment-variables), not files in the archive. ## Related - [Deploy with AI](/deploy-with-ai/overview): Claude and Copilot deploys, end to end. - [Build settings](/create/build-settings): The settings detection fills for an uploaded project. - [Deployments](/platform/deployments): What connecting a repository adds on top. --- # Deployments Connect a GitHub, GitLab or Bitbucket repository and every push builds and deploys. ## Connect a repository Sign in with GitHub, GitLab or Bitbucket and pick a repository. On GitHub, Light Cloud installs a GitHub App with access to the repositories you choose; on GitLab and Bitbucket, it connects through your user and creates a webhook on each project or repository you deploy. Either way it can read the code and build on push: each commit to a connected branch triggers a build of that branch, and the branch list is offered at create time. How each provider connects, what access it needs, and how starter templates land in your own repository: [Git providers](/platform/deployments/git-providers). ## Detection Light Cloud reads the repository to decide how to build it: the framework, the package manager, the build command, and the output directory. See [Frameworks](/platform/frameworks#how-detection-works) for how detection works and the full list. ## Build settings Settings live on the application, and every environment can override them - production and a preview can build differently: - **Framework, build command, output directory** - what gets built and where the output lands. See [Build settings](/create/build-settings). - **Runtime and container port** - for backends, the language runtime and the port your server listens on. - **Size and scaling** - memory, CPU, and minimum and maximum instances. The minimum defaults to zero, so idle apps [scale to zero and stop billing](/billing/billing#scale-to-zero). - **Region** - Belgium, Iowa, South Carolina, or Oregon, chosen when you create the app. - **Environment variables** - see [Environment variables](/platform/environment-variables). ## Build and release - A **static** build is uploaded to object storage and served from a global CDN. - A **container** build becomes an image on Cloud Run that scales with traffic. What runs inside the build - steps, generated Dockerfiles, caching, failures - is on its own page: [Builds](/platform/deployments/builds). Every deployed environment gets a URL: ``` {branch}-{app}-{org}.light-cloud.io ``` Attach your own hostname with a [custom domain](/platform/custom-domains). ## The production environment The `main` (or `master`) branch becomes the environment named **Production**. It is marked, protected from automatic cleanup, and sorted first in the console. Everything else is a [preview environment](/platform/preview-environments) you can create and remove freely. ## Deploy history The last 20 deploys of each environment are kept, each with its commit SHA, message, author, duration, status, and build steps. Shipping again and shipping backwards - **Redeploy**, reverts, manual releases - is covered on [Rollbacks](/platform/deployments/rollbacks). ## Deploy without a repo You can deploy a local folder without a repository - from the Create page, the VS Code extension, or the MCP server. See [Deploy from an upload](/create/upload). ## Go deeper - [Git providers](/platform/deployments/git-providers): GitHub, GitLab and Bitbucket - connecting, access, pushes, and template copies. - [Builds](/platform/deployments/builds): The build pipeline - steps, Dockerfile generation, caching, failures. - [Rollbacks](/platform/deployments/rollbacks): Redeploy, revert, and releasing by hand. - [Preview environments](/platform/preview-environments): An environment and URL for every branch. - [Git automation](/create/git-automation): The toggles that drive all of this. --- # Git providers Light Cloud deploys from **GitHub**, **GitLab** and **Bitbucket**. All three give you the same thing - every push builds and deploys, every branch can have a preview, and starter templates become a repository you own - through the mechanism each provider offers. | | GitHub | GitLab | Bitbucket | | --- | --- | --- | --- | | Sign in / sign up | Yes | Yes | Yes | | How it connects | A GitHub App installed on your account or organisation | An OAuth grant from a GitLab user | An OAuth grant from a Bitbucket user | | What it can see | The repositories you select in the installation | Every project that user is a member of | Every repository that user can read, across their workspaces | | Deploy on push | The App's webhook | A push hook created on each project | A webhook created on each repository | | Private repositories | Yes | Yes | Yes | | Starter templates | Copied into a new private repository under your account | Imported into a new private project under your user | Pushed into a new private repository in your workspace | | Deploy feedback | Check run, deployment status and a preview comment on pull requests | Commit status, environment deployment and a preview note on merge requests | Build status on the commit and a preview comment on pull requests | | Instances | github.com | gitlab.com | bitbucket.org | ## GitHub Sign in with GitHub, or connect it later from the GitHub badge in the header. Light Cloud installs a **GitHub App**: you pick which repositories it may see, and can change the selection any time in GitHub's own settings. One installation can serve several workspaces, and an owner links or unlinks it per workspace. Pushes arrive as signed webhooks from the App, so nothing is configured per repository. Deploys are also reported back to GitHub as check runs and deployment statuses - see [Deployments](/platform/deployments). ## GitLab Sign in with GitLab, or connect it from the GitLab badge in the header or from the Create page. GitLab has no App concept, so the connection is an **OAuth grant** from your GitLab user with the `api` and `read_user` scopes: `api` is what creating push hooks and reading private projects requires, `read_user` identifies the account. - **Workspace connection.** An owner connects GitLab for a workspace; every member can then deploy any project that GitLab user can reach. The same grant can serve several workspaces, and unlinking removes only that workspace's link. Disconnecting removes the grant itself. - **Pushes.** When you create an app from a GitLab project, Light Cloud creates a push hook on that project. Pushes to the deployed branch build and deploy; pushes to other branches drive [preview environments](/platform/preview-environments) exactly as on GitHub. Deleting the app removes the hook. - **Branches.** The project's branch list is offered at create time, so production can start from any branch, not only the default. - **One GitLab account per Light Cloud user.** If the GitLab account you are connecting already belongs to another Light Cloud account, the connect is refused and you are offered to move it - the other account loses the connection, and the workspaces it served keep deploying through yours. - **Revoked access.** If you revoke Light Cloud in GitLab, the connection is dropped on the next use and the badge asks you to reconnect. Nothing already deployed stops. - **Empty projects.** A project with no commits cannot be deployed - GitLab reports a default branch for it that does not exist yet. Push a first commit, then create the app. - **Deploy feedback.** Each deploy posts a commit status named `Light Cloud - ` (the tick or cross on the commit, in the merge-request widget and in the pipeline view, so it counts toward "Pipelines must succeed"), records a deployment on a GitLab environment (`production` or `preview/` under Operate, Environments, with an Open button on the preview URL), and keeps one note per environment on each open merge request with the preview link. Both are switched per app in Settings, GitLab card. The connected GitLab user needs the Developer role or higher on the project. Self-managed GitLab instances are not supported yet; the integration talks to gitlab.com. ## Bitbucket Sign in with Bitbucket, or connect it from the Bitbucket badge in the header or from the Create page. Like GitLab, Bitbucket Cloud has no App concept: the connection is an **OAuth grant** from your Bitbucket (Atlassian) user, and Light Cloud asks for the account, email, repository (read, write, admin), project (read), pull request (read, write) and webhook scopes. Repository admin and project read are what creating a repository for a starter copy needs; write is what pushing that copy needs; the rest is listing, building and reporting. - **Workspace connection.** An owner connects Bitbucket for a workspace; every member can then deploy any repository that Bitbucket user can read, in any Bitbucket workspace they belong to. The same grant can serve several Light Cloud workspaces, and unlinking removes only that workspace's link. Disconnecting removes the grant itself. - **Pushes.** When you create an app from a Bitbucket repository, Light Cloud creates a webhook on that repository, signed with a secret it verifies on every delivery. Pushes to the deployed branch build and deploy; pushes to other branches drive [preview environments](/platform/preview-environments) exactly as on GitHub. Deleting the app removes the webhook. - **Branches.** The repository's branch list is offered at create time, so production can start from any branch, not only the main one. - **One Bitbucket account per Light Cloud user.** If the Bitbucket account you are connecting already belongs to another Light Cloud account, the connect is refused and you are offered to move it - the other account loses the connection, and the workspaces it served keep deploying through yours. - **Revoked access.** If you revoke Light Cloud in Bitbucket, the connection is dropped on the next use and the badge asks you to reconnect. Nothing already deployed stops. - **Empty repositories.** A repository with no commits has nothing to deploy. Push a first commit, then create the app. - **Monorepos.** Bitbucket's push payload does not list the changed files, so an app with a [root directory](/create/application-settings#root-directory) rebuilds on every push to its branch, not only when that folder changed. - **Deploy feedback.** Each deploy posts a build status named `Light Cloud - ` on the commit (the tick, cross or spinner on the commit, in the branch list and in the pull request's Builds column, so it counts toward "minimum successful builds" merge checks), updated on every build step, and keeps one comment per environment on each open pull request with the preview link. Both are switched per app in Settings, Bitbucket card. Bitbucket's Deployments dashboard is fed by Pipelines only, so deployments are not recorded there; the preview URL is in the comment and the status link. The connected Bitbucket user needs write access to the repository. - **Read-only workspaces.** A Bitbucket workspace over its plan's user limit is read-only: pushes, including a starter copy, are refused by Bitbucket until the plan or membership is fixed on bitbucket.org. Bitbucket Server and Data Center are not supported; the integration talks to bitbucket.org. ## Starter templates A [starter template](/create/templates) is copied into a repository you own before the first deploy, so pushes to your copy drive the app: - With GitHub connected, the copy is a private repository under the GitHub account shown on the Create page. - With GitLab connected, the template is imported into a new private project under your GitLab user. - With Bitbucket connected, the template is pushed into a new private repository in the Bitbucket workspace you administer. - With none connected, the app deploys read-only from the shared template and auto-deploy stays off. When more than one provider is connected to the workspace, the Create page's **Repository** row lets you choose which account receives the copy; the default is the first connected in the order GitHub, GitLab, Bitbucket. Only connections linked to the Light Cloud workspace are offered - unlinking a provider removes it from the choice. If the copy cannot be created (Bitbucket refusing the push, for example), the create stops with the provider's message instead of deploying the shared template. ## Which one is being used The provider's mark is shown next to every repository reference - the application header, the environment's Source card, the environments list, each deployment's commit, and the deployer of a webhook-triggered deploy - and the header badges show each connection's state: green connected, amber not connected but you can connect it, grey not connected and only an owner can. ## Related - [Deployments](/platform/deployments): Connecting a repository, detection, build settings, and history. - [Git automation](/create/git-automation): Deploy on push, a preview per branch, and cleanup. - [Templates](/create/templates): Starters and stacks, and where your copy lands. --- # Builds Every deploy starts with a build: the commit is turned into either a folder of static files or a container image. This page is what happens inside that step. ## The steps A build runs on Cloud Build and streams its progress into the deploy record: 1. **Fetch.** The commit is checked out. Apps with a [root directory](/create/application-settings#root-directory) build from that folder. 2. **Install.** Dependencies are installed with the [install command](/create/build-settings#install-command) - by default, the package manager your lockfile implies. 3. **Build.** The [build command](/create/build-settings#build-command) runs. For containers this happens inside the image build. 4. **Package.** A static build's [output directory](/create/build-settings#output-directory) is uploaded to object storage; a container image is pushed to Artifact Registry. 5. **Release.** The CDN starts serving the new files, or Cloud Run shifts traffic to the new revision. Watch it live in the [build logs](/platform/observability#build-logs); each deploy keeps its full log. ## Dockerfiles: generated or your own A `Dockerfile` in the repository **always wins** - the app builds and runs exactly as written, whatever is inside. See [Deploy with a Dockerfile](/platform/frameworks/docker). Without one, Light Cloud generates a Dockerfile for the detected framework: - Generated Node images run on **Node 22**. - The install step follows your lockfile - `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json` decide the package manager, and with a lockfile present installs are exact (`npm ci` and equivalents). - The image runs your server on the configured [port](/create/build-settings#port), with `PORT` set in the environment. ## Environment variables at build time - **Static builds** run the build command with the environment's [variables](/platform/environment-variables#build-time-and-runtime) set - inlined values (`NEXT_PUBLIC_*`, `VITE_*`) work as usual. - **Container images** are built *without* your runtime variables. Read configuration at startup, not at image build time - the same image may serve an environment whose variables have changed since. ## When a build fails The deploy is marked failed and nothing is released - the previous deploy keeps serving. The lines that broke the build are pulled out of the build output and put on the deploy record, so the reason is on screen without digging. Common causes, in the order they usually bite: | Symptom | Usual cause | Fix | | --- | --- | --- | | Install step fails | Lockfile out of sync with `package.json` | Reinstall locally, commit the lockfile | | Build command fails | Type errors, missing build-time variable | Read the extracted lines; set the variable on the [environment](/platform/environment-variables) | | Output directory empty | Framework writes somewhere else | Match [output directory](/create/build-settings#output-directory) to your config | | Green build, unreachable app | Server listens on the wrong port | Match the [port](/create/build-settings#port) or read `PORT` at startup | A failed build costs its [build minutes](/billing/billing#what-counts-as-usage) and nothing else. ## Related - [Build settings](/create/build-settings): The four fields these steps read. - [Rollbacks](/platform/deployments/rollbacks): Shipping again after a fix. - [Observability](/platform/observability): Build logs and runtime logs, side by side. --- # Rollbacks Something bad shipped, or something good needs shipping again. Three moves cover it. ## Redeploy **Redeploy** on an environment ships the branch tip again, through the normal [build pipeline](/platform/deployments/builds). Use it when the code is fine but the deploy context changed: - After editing [environment variables](/platform/environment-variables) - though saving a changed set redeploys automatically. - After changing build settings, size, or scaling. - After a build failed for a transient reason. ## Roll back code To undo a bad commit, revert it in Git and push: ```bash git revert HEAD git push ``` The pipeline treats the revert like any other change and has it live in one build. This keeps Git as the single source of truth - the deployed code always matches the branch, and the rollback itself shows up in [deploy history](/platform/deployments#deploy-history) with its own commit. > [!NOTE] > A failed build never replaces the running deploy - the previous deploy keeps serving until a build succeeds. Bad builds don't need rolling back, only bad code that built successfully does. ## Manual releases Turn off [deploy on push](/create/git-automation#deploy-on-push) and nothing ships until you press **Redeploy**. Pushes still update the repository; the environment waits. This suits scheduled releases: merge all week, ship on Thursday. ## What history shows Each environment keeps its last 20 deploys - commit SHA, message, author, duration, status, and build steps. That is the audit trail for "what was live when": find the deploy that introduced a problem, open its commit, revert it. ## Related - [Deploy history](/platform/deployments#deploy-history): What is kept for every environment. - [Builds](/platform/deployments/builds): Why a build fails and how to read it. - [Git automation](/create/git-automation): The toggles behind automatic deploys. --- # Preview environments Every branch can have its own environment with its own URL, built from that branch. Preview URLs are unlimited on every plan, Free included. ## How previews work 1. Push a branch. Light Cloud creates an environment for it and deploys it. 2. The environment gets its own URL, named after the branch: ``` {branch}-{app}-{org}.light-cloud.io ``` A branch called `feature-checkout` on app `storefront` in org `acme` serves at `feature-checkout-storefront-acme.light-cloud.io`. Drop the link into the pull request and reviewers click instead of pulling the branch. 3. Every further push to the branch redeploys its environment. 4. Delete the branch and the environment is removed with it. Stale previews do not pile up. ## Settings Three toggles control the lifecycle, all on by default: - **Auto-create environment per branch** (application) - new branches get an environment on their first push. - **Auto-cleanup when branch is deleted** (application) - deleting a branch deletes its environment. Never touches production. - **Auto-deploy on push** (per environment) - turn it off on an environment to deploy it only when you say so. All three are explained field by field under [Git automation](/create/git-automation). ## Environment variables A preview starts from the application's variables and can override any of them - point a branch at a test database while production keeps the real one. See [Environment variables](/platform/environment-variables#per-app-and-per-environment). > [!TIP] > On Starter and up, give previews a [Shared-tier database](/platform/databases#the-shared-tier) of their own instead of sharing production data: it is ready in seconds, costs about $4 a month, and a preview can never corrupt what production reads. Lite allows one database in total. ## A long-lived staging environment Previews don't have to be short-lived. Create an environment for a `staging` or `develop` branch and keep it: it deploys on every push to that branch, carries its own variables and domain, and behaves like a second production - minus the protection. ## Protect a preview Give an environment a visitor password in its settings and the URL asks for it before serving. Handy for demos and unreleased features that should not be public while they are reviewed. ## Production is protected Each application has one production environment, marked and protected. Branch activity does not touch it: automatic cleanup skips it, and only its own branch deploys to it. See [The production environment](/platform/deployments#the-production-environment). ## Cost Container previews [scale to zero](/create/scaling#min-instances) when no one is using them, so idle review apps cost nothing. A preview is billed only while someone is actually looking at it. A static preview counts its build minutes - about two cents for a couple of builds - and a little storage; its bandwidth is included. See [Billing](/billing/billing#scale-to-zero). Previews do not count against a plan's server-app limit: the limit counts apps, not branches. ## Related - [Git automation](/create/git-automation): The three toggles, field by field. - [Environment variables](/platform/environment-variables): Overriding configuration per environment. - [Custom domains](/platform/custom-domains): A real hostname on any environment, previews included. --- # Databases Provision managed PostgreSQL 15 or MySQL 8.0 from the same project you deploy from. Databases run on Cloud SQL in the same regions as your apps: Belgium, Iowa, South Carolina, and Oregon. ## Tiers | Tier | vCPUs | RAM | ~Per month | | --- | --- | --- | --- | | Dev (shared) | Pooled | Pooled | $4 | | Starter | Shared core | 0.6 GB | $9 | | Pro | Shared core | 1.7 GB | $26 | | Business | 1 | 4 GB | $56 | | Enterprise | 2 | 8 GB | $111 | | EnterprisePlus | 4 | 16 GB | $221 | Starter and Pro are shared-core instances for development. Business and up are dedicated instances meant for production. You can change the tier and storage as the app grows. Prices are the hourly rates over a full month; see [Pricing](/billing/pricing#databases). Databases start on Lite, which allows one Dev (shared) database. Starter and up can create as many as they need, on the tiers the plan allows; Free has no databases. The full list is under [Limits](/reference/limits#plans). A Shared database is ready in seconds. A dedicated instance takes ten to twenty minutes to provision - start it before you need it. ## The Shared tier The cheapest way to get a real database. Your database lives on a pooled instance with its own credentials and 1 to 10 GB of storage, for about $4 a month. Available in Belgium (`europe-west1`). Shared databases get their own hostname (`{db}-{org}.db.light-cloud.io`) and require TLS on every connection. Ideal for side projects, [previews](/platform/preview-environments), and experiments - move to a dedicated tier when the app needs one. > [!NOTE] > On the Shared tier the database name is generated opaque on purpose: co-tenants of a pooled instance can list database names, so yours must reveal nothing. Read it from the Credentials tab. See [Database settings](/create/database-settings#connection-details). ## Storage Dedicated tiers come with 10 to 100 GB of SSD storage at $0.30 per GB per month, on top of the tier price. Storage can be grown later without downtime, but never shrunk - start small. The available sizes per tier are listed under [Database settings](/create/database-settings#storage). ## Backups and high availability Every database is backed up automatically each day, and PostgreSQL keeps point-in-time recovery on top of that. There is nothing to configure or remember. Restores, exports, and imports are on [Backups](/platform/databases/backups). Turn on high availability for workloads that cannot take downtime. HA doubles the compute price. ## Connect The database's **Credentials** tab shows the host, port, database name, user, and password, plus a ready-made connection string with a copy button: ``` postgresql://appuser:PASSWORD@203.0.113.10:5432/mydb?sslmode=require ``` Put it in the app's [environment variables](/platform/environment-variables) as `DATABASE_URL` and most frameworks connect out of the box. The full walkthrough - CLI access, TLS, framework examples, previews with their own databases - is on [Connect an app](/platform/databases/connect). ## Full stack in one place Frontend, backend, and database live in the same project. You deploy from GitHub, and the database is one environment variable away. ## Go deeper - [Connect an app](/platform/databases/connect): Connection strings, TLS, CLI access, and framework examples. - [Backups](/platform/databases/backups): Daily backups, point-in-time recovery, import and export. - [Database settings](/create/database-settings): Every field on the create form. - [Pricing](/billing/pricing): Hourly rates for tiers, storage, and high availability. --- # Connect an app Everything needed to connect lives on the database's **Credentials** tab: host, port, database name, user, and password, plus a ready-made connection string with a copy button. ``` postgresql://appuser:PASSWORD@203.0.113.10:5432/mydb?sslmode=require ``` ## Set DATABASE_URL Put the connection string in the app's [environment variables](/platform/environment-variables) - most frameworks and ORMs take a single `DATABASE_URL`: ``` DATABASE_URL=postgresql://appuser:PASSWORD@203.0.113.10:5432/mydb?sslmode=require ``` That covers Prisma, Drizzle, Rails, Laravel, Django (with `dj-database-url`), and most Node, Python, and Go database libraries. Frameworks that want separate values instead - host, user, password, name - take the same fields from the Credentials tab as individual variables. WordPress apps get a shortcut: pick the database in the app's settings and the `WORDPRESS_DB_*` variables are filled in for you. [Stacks](/create/templates#stacks) arrive wired already. ## TLS Shared-tier databases **require** TLS on every connection - keep `sslmode=require` (PostgreSQL) or your driver's TLS option (MySQL) in place. Dedicated instances accept TLS too (minimum version 1.2); leave it on unless you have a reason not to. ## One database per environment Point production at the real database and give [previews](/platform/preview-environments) a database of their own, by overriding `DATABASE_URL` per environment: - **Production:** the dedicated instance. - **Previews:** a [Shared-tier database](/platform/databases#the-shared-tier) - ready in seconds, about $4 a month, and disposable. A preview then can't corrupt production data, and schema experiments stay contained. ## Connect from your laptop The same connection string works from your machine: ```bash psql "postgresql://appuser:PASSWORD@203.0.113.10:5432/mydb?sslmode=require" ``` ```bash mysql --host=203.0.113.10 --port=3306 --user=appuser --password --ssl-mode=REQUIRED mydb ``` Run migrations, inspect data, or take an extra dump - it is a normal PostgreSQL or MySQL endpoint. ## Keep the region close Every query crosses the distance between the app and the database. Create both in the same [region](/create/application-settings#region) - the create form warns when they differ. > [!TIP] > Connect lazily. Open the database connection on first use rather than at process start: cold starts get faster, and an app at [zero instances](/create/scaling#min-instances) holds no idle connections. ## Related - [Databases](/platform/databases): Tiers, the Shared tier, storage, and high availability. - [Backups](/platform/databases/backups): Daily backups, point-in-time recovery, import and export. - [Environment variables](/platform/environment-variables): Where DATABASE_URL lives, per environment. --- # Backups Every database is backed up automatically each day. There is nothing to configure, schedule, or remember. PostgreSQL databases keep **point-in-time recovery** on top of the daily backups, so they can be restored to a moment, not just to last night. ## Export The **Backup** tab exports the database as a `.sql` (or compressed `.sql.gz`) dump - the standard format `psql` and `mysql` understand. Use it for: - An extra copy before a risky migration. - Moving data between databases - a dump from one imports into another. - Taking your data with you. It is a plain SQL file; nothing about it is platform-specific. ## Import The Backup tab also imports `.sql` and `.sql.gz` dumps into an existing database. Creating a new database? The Shared tier can import a dump **at create time** - pick the file on the create form and it loads the moment the database is ready. Dedicated instances provision for 10-20 minutes, so their imports run from the Backup tab once ready. See [Seed from dump](/create/database-settings#seed-from-dump). If an import fails, the database survives as it was and the import can be retried from the Backup tab. ## Restore - **PostgreSQL** can be restored to a point in time - to just before the bad migration, not just to last night's backup. - **MySQL** restores from the daily backups. - Any database can be rebuilt from an exported dump: create a new database, import, and repoint [`DATABASE_URL`](/platform/databases/connect#set-database_url). > [!IMPORTANT] > Storage can be grown without downtime but never shrunk, and restores need room. If a restore or import fails on space, grow the storage first - see [Database settings](/create/database-settings#storage). ## A move-in, step by step Migrating an existing database onto the platform: 1. Dump it at the old provider: `pg_dump --format=plain mydb > backup.sql` (or `mysqldump`). 2. Create a [database](/create/database-settings) - on the Shared tier, attach the dump on the create form; on a dedicated tier, import from the Backup tab when it is ready. 3. Set the new connection string on your app's [environments](/platform/environment-variables). 4. Redeploy, verify, and retire the old database. ## Related - [Databases](/platform/databases): Tiers, storage, and high availability. - [Connect an app](/platform/databases/connect): Connection strings and per-environment databases. - [Database settings](/create/database-settings): Seeding a new database from a dump. --- # Custom domains Point a domain at any application or environment. Certificates are issued and renewed for you - there is nothing to install or rotate. ## Default URLs Every deployed environment already has a URL, built from the branch, the app, and your organization: ``` {branch}-{app}-{org}.light-cloud.io ``` The `main` branch of an app called `storefront` in the `acme` org serves at `main-storefront-acme.light-cloud.io`. These subdomains are covered by a wildcard certificate and need no DNS setup. A custom domain sits on top of this; remove it and the environment reverts to its subdomain. ## Add a domain Custom domains come with every paid plan, from Lite at $5 a month, with SSL included. On Free, apps keep their light-cloud.io address, and adding a domain is refused with the plan that includes it. A domain that is already on a Free workspace, attached before custom domains became paid or kept after moving to Free, keeps working for 14 days after we email the owner. After that it goes on hold: visitors see a holding page, the site keeps serving at its light-cloud.io address, and moving to a paid plan brings the domain back within a minute. 1. Deploy first. A domain can only be attached to an app or environment that is live. 2. Open the **Domain** tab of the environment and enter the address people should use, without `https://`: `www.example.com`, `app.example.com`, or the bare `example.com`. 3. For `www.example.com` or `example.com`, both are set up at once: the one you entered is the main address, the other redirects to it (over HTTPS, keeping the path). A subdomain such as `app.example.com` is set up on its own. 4. The tab lists the DNS records per hostname. Each record shows the name the way your provider's form asks for it (`www`, `@` for the root), the full name under it, what the record is for, and a copy button: | Hostname | Type | Name | Value | Needed | | --- | --- | --- | --- | --- | | `www.example.com` | CNAME | `www` | `light-cloud.io` | required | | `www.example.com` | TXT | `_acme-challenge.www` | shown in the console | optional | | `example.com` | ALIAS / ANAME | `@` | `light-cloud.io` | required | | `example.com` | TXT | `_acme-challenge` | shown in the console | required | - **Required** records are all a domain needs. For a subdomain (`www`, `app`) that is the one CNAME: the certificate is issued on its own once the name points here. - **Optional** records sit behind "optional records: switch without downtime". They matter only when the domain already has visitors on another host: add them first, wait for **Active**, then change the CNAME. They can be deleted once the domain is active. - A root domain keeps its TXT records required. - A **CAA** record appears only when your domain already limits who may issue its certificates. 5. If the tab shows **Delete these records**, remove them at your provider: an old A record, or a registrar's forwarding or parking, sends the domain somewhere else and the new record cannot take effect beside it. 6. The tab checks your DNS when it opens and again on **Check again**. Each record is marked found, not found, or different value with what public DNS answers now. DNS changes usually land in minutes but can take up to 48 hours. Each hostname is marked active once its records are in place and the certificate is issued. From then on, traffic to the domain reaches that environment over HTTPS. ## Root domains (example.com) A root domain cannot carry a CNAME. It needs an **ALIAS** record - some providers call it ANAME or CNAME flattening. The Domain tab detects your DNS provider and says what to do: - **Has ALIAS / ANAME** (Name.com, Namecheap, Cloudflare, DNSimple, Vercel DNS, ...): add the root as that record type, pointing at `light-cloud.io`. - **No ALIAS** (GoDaddy, Squarespace, Route 53, OVH, Hover, DigitalOcean, ...): use `www.example.com` as the main address. The root still works for `http://` links through your provider's forwarding, but not for `https://`, so it cannot be the main address. Do not use a registrar's "URL forwarding" or "URL redirect" for a root that should serve HTTPS: those services answer plain HTTP only, so `https://example.com` fails in every browser. ## One domain, one target A domain can be attached to exactly one application or environment across the platform. Point `example.com` at production and give staging its own hostname, say `staging.example.com`, on the staging environment. Each domain gets its own certificate (TLS 1.2 minimum). Domains work on [preview environments](/platform/preview-environments) too - a long-lived staging branch with a real hostname is a normal setup. ## Certificates - Issued automatically once DNS verification passes; renewed automatically before expiry. - HTTPS is always on - the default subdomains and every custom domain serve TLS 1.2 or newer. - Nothing to upload, install, or rotate. There is no way to bring your own certificate, and no need to. ## Troubleshooting - **Stuck on pending.** Click **Check again**: it names the record that is still missing or has a different value, and lists records to delete. Fix those at your provider and check again. Propagation can take up to 48 hours depending on the provider and the record TTLs. - **`https://example.com` fails, `www` works.** The root still points at a forwarding service that has no HTTPS. Replace it with an ALIAS / ANAME record to `light-cloud.io`, or make www the main address. - **Certificate is not issued.** If your domain has CAA records, add the CAA records the tab lists. - **Root redirect never turns active.** On a provider with no ALIAS record the root is marked optional: the main `www` address is complete without it. - **Verification failed.** Use **Retry connection** on the domain tab - it discards the pending setup and starts verification fresh. - **Wrong domain.** Use **Change**, or remove the domain and add the right one; the environment falls back to its `*.light-cloud.io` subdomain in the meantime, so nothing goes dark. - **Works on the subdomain, not the domain.** The CNAME is missing or still propagating - `dig CNAME app.example.com` should answer `light-cloud.io`. > [!NOTE] > Subdomains of `light-cloud.io` are reserved - a custom domain has to be one you own. ## Related - [Deployments](/platform/deployments): Where the default URLs come from. - [Preview environments](/platform/preview-environments): Passwords and per-branch URLs for unreleased work. --- # Frameworks Light Cloud reads your repository and picks the right build. Static frontends go to the CDN, everything else runs as a container on Cloud Run. This page is the complete list of what detection recognises. ## Full-stack Server-rendered apps run as containers that [scale to zero](/create/scaling#min-instances) when idle: Every framework name links to its deploy guide. | Framework | Runtime | Default port | Notes | | --- | --- | --- | --- | | [Next.js](/platform/frameworks/nextjs) | Node.js | 3000 | Static export serves from the CDN | | [Nuxt](/platform/frameworks/nuxt) | Node.js | 3000 | | | [SvelteKit](/platform/frameworks/sveltekit) | Node.js | 3000 | | | [Remix](/platform/frameworks/remix) | Node.js | 3000 | | | [Astro](/platform/frameworks/astro) | Node.js | - | Static output, served from the CDN | | [Django](/platform/frameworks/django) | Python | 8000 | | | [Ruby on Rails](/platform/frameworks/rails) | Ruby | 3000 | | | [Laravel](/platform/frameworks/laravel) | PHP | 8080 | | | [Symfony](/platform/frameworks/symfony) | PHP | 8080 | | | [WordPress](/platform/frameworks/wordpress) | PHP | 8080 | One-click [database wiring](/platform/databases/connect#set-database_url); also a [stack](/create/templates#wordpress) | | [Blazor](/platform/frameworks/blazor) | .NET | 8080 | | | [Wasp](/platform/frameworks/wasp) | Node.js | 8080 | Powers the [Open SaaS template](/create/templates#open-saas) | ## Frontend Static builds served from a global CDN - unlimited on every plan, with [bandwidth included](/billing/pricing#static-sites) up to a fair-use amount per workspace. Detection also knows each framework's default [output directory](/create/build-settings#output-directory): | Framework | Output directory | | --- | --- | | [React](/platform/frameworks/react) | `dist` | | [Vue](/platform/frameworks/vue) | `dist` | | [Angular](/platform/frameworks/angular) | `dist/browser` | | [Svelte](/platform/frameworks/svelte) | `dist` | | [SolidJS](/platform/frameworks/solidjs) | `dist` | | [Qwik](/platform/frameworks/qwik) | `dist` | | [Gatsby](/platform/frameworks/gatsby) | `public` | | [Docusaurus](/platform/frameworks/docusaurus) | `build` | | [Eleventy](/platform/frameworks/eleventy) | `_site` | | [Astro](/platform/frameworks/astro) | `dist` | | [Next.js static export](/platform/frameworks/nextjs) | `out` | | [Plain HTML, CSS, and JavaScript](/platform/frameworks/static-html) | `.` (the repository root) | Hugo and Jekyll are not built on the platform yet - each needs its own toolchain in the build image. Build locally and deploy the output as static HTML. ## Backend APIs and servers run as containers, on any of eight runtimes. A plain runtime with no framework works too - a bare Node script or Go module deploys the same way: | Framework | Runtime | Default port | | --- | --- | --- | | [Express](/platform/frameworks/express) | Node.js | 8080 | | [Fastify](/platform/frameworks/fastify) | Node.js | 3000 | | [NestJS](/platform/frameworks/nestjs) | Node.js | 3000 | | [AdonisJS](/platform/frameworks/adonisjs) | Node.js | 3333 | | [Hono](/platform/frameworks/hono) | Node.js | 3000 | | [Plain Node.js](/platform/frameworks/nodejs) | Node.js | 8080 | | [FastAPI](/platform/frameworks/fastapi) | Python | 8000 | | [Flask](/platform/frameworks/flask) | Python | 8000 | | [Plain Python](/platform/frameworks/python) | Python | 8000 | | [Gin](/platform/frameworks/gin) | Go | 8080 | | [Echo](/platform/frameworks/echo) | Go | 8080 | | [Fiber](/platform/frameworks/fiber) | Go | 8080 | | [Plain Go](/platform/frameworks/go) | Go | 8080 | | [Spring Boot](/platform/frameworks/spring-boot) | Java | 8080 | | [Quarkus](/platform/frameworks/quarkus) | Java | 8080 | | [Plain Java](/platform/frameworks/java) | Java | 8080 | | [Sinatra](/platform/frameworks/sinatra) | Ruby | 3000 | | [Plain Ruby](/platform/frameworks/ruby) | Ruby | 3000 | | [ASP.NET](/platform/frameworks/aspnet) | .NET | 8080 | | [Plain PHP](/platform/frameworks/php) | PHP | 8080 | | [Dockerfile](/platform/frameworks/docker) | Any | 8080 | The port is where traffic is routed and what `PORT` is set to - change it in [Build settings](/create/build-settings#port) if your server listens elsewhere. A repository with a Dockerfile runs as is, whatever is inside - see [Deploy with a Dockerfile](/platform/frameworks/docker). ## How detection works Light Cloud reads the repository and works out the whole build without configuration: - **Framework** - from dependencies and config files. `next.config.js`, `vite.config.ts`, `angular.json`, and their peers are parsed to find where the build output lands. - **Package manager** - from the lockfile. `pnpm-lock.yaml`, `yarn.lock`, or `package-lock.json` decide whether the install step runs pnpm, yarn, or npm - with a lockfile present, installs are exact (`npm ci`). - **Build command and output directory** - from the framework and your `package.json` scripts. The defaults per framework are tabled under [Build settings](/create/build-settings#output-directory). - **Dockerfile** - a `Dockerfile` in the repo always wins and runs the app as a container, exactly as written. Without one, Light Cloud generates a Dockerfile for the detected framework; generated Node images run on Node 22. See [Builds](/platform/deployments/builds#dockerfiles-generated-or-your-own). Every detected value is shown before the first deploy and can be overridden - per application, and again per environment. See [Deployments](/platform/deployments). ## Not sure what you'll get? Create the application and look at the detected settings before deploying - nothing builds until you confirm. If your setup is unusual, set the build command and output directory by hand and the detection stays out of the way. ## Deploy guides Every framework above has its own deploy guide - click its name in the tables, or expand **Frameworks** in the sidebar for the full list. The most-trodden paths: - [Next.js](/platform/frameworks/nextjs): SSR behind the edge, or static export on the CDN. - [React](/platform/frameworks/react): Vite or CRA, built to static files on the CDN. - [Django](/platform/frameworks/django): Settings, migrations, and static files, containerized. - [Express](/platform/frameworks/express): The classic Node API on scale to zero. - [WordPress](/platform/frameworks/wordpress): One-click database wiring, or the full stack template. - [Dockerfile](/platform/frameworks/docker): Bring your own image recipe for anything else. --- # Deploy Next.js Next.js deploys in one of two shapes, and detection picks the right one from your `next.config.js`: | Shape | When | Serves from | | --- | --- | --- | | **Server-rendered** (default) | Anything with server code: SSR, API routes, server actions | A container behind the edge CDN | | **Static export** | `output: 'export'` in the config | The CDN only - no server at all | ## Server-rendered Push a Next.js repo and it builds into a container (generated image on Node 22, listening on port `3000`). Server-rendered frontends run under [the SSR policy](/create/scaling#the-ssr-policy): pinned to the **Micro** size, scaling 0-3, in a tier-1 region, always behind the edge CDN. The edge serves your audience; the origin only renders cache misses, so a bigger origin would buy nothing. Things to know: - **Port.** The server must listen where the platform routes - the generated setup handles it, and `next start` respects the `PORT` [variable](/create/build-settings#port), which is set for you. - **Runtime configuration.** Server code reads `process.env` at runtime as usual - set values as [environment variables](/platform/environment-variables), per environment. - **Build-time inlining.** Container images are [built without your runtime variables](/platform/deployments/builds#environment-variables-at-build-time), so `NEXT_PUBLIC_*` values are inlined from what exists at image build, not from the environment's variable set. Read configuration server-side at runtime where you can; if the client bundle must carry environment-specific values, prefer the static export shape, whose build runs with the variables set. ## Static export Set the export mode in `next.config.js`: ```js /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', }; module.exports = nextConfig; ``` The build writes plain files into `out`, and that folder is served from the CDN - [bandwidth included](/billing/pricing#static-sites), no region, no cold starts, no instance to pay for. `NEXT_PUBLIC_*` variables work normally here, because [static builds run with the environment's variables set](/platform/environment-variables#build-time-and-runtime). Static export drops the server features: no SSR, no API routes, no server actions. If you need an API next to a static frontend, deploy it as its own [backend app](/platform/frameworks#backend) - a monorepo with two apps and two [root directories](/create/application-settings#root-directory) works well. ## Which shape to pick - Content sites, docs, dashboards against an external API: **static export**. Cheapest and fastest. - Apps that render per-request, use API routes, or need server actions: **server-rendered**. You can switch later - change the config, push, and detection follows. ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Scaling](/create/scaling): The SSR policy and what scale to zero means. - [Environment variables](/platform/environment-variables): Build-time vs runtime values, in detail. --- # Deploy Nuxt Nuxt is detected from its dependency and `nuxt.config.ts`, and runs as a Node.js container on port `3000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) on Node 22 unless the repo has its own. ## How it runs Nuxt is a server-rendered frontend, so it runs under [the SSR policy](/create/scaling#the-ssr-policy): pinned to the **Micro** size, scaling 0-3, in a tier-1 region, always behind the edge CDN. The edge serves your audience; the origin renders cache misses. - **Build.** `npm run build` produces the `.output` server bundle; the container starts it with Node. - **Port.** The server respects `PORT`, which is set for you - no config needed. - **Cold starts.** At zero instances the first request boots the server; keep heavy work lazy. See [Cold starts](/create/scaling#cold-starts). ## Configuration Server-side code reads [environment variables](/platform/environment-variables) at runtime - `runtimeConfig` values fed by `NUXT_*` variables work as usual, per environment. Public values that must be inlined into the client bundle follow the container rule: images are [built without your runtime variables](/platform/deployments/builds#environment-variables-at-build-time). Prefer `runtimeConfig.public` (delivered at runtime) over build-time inlining. ## Database Provision [managed PostgreSQL or MySQL](/platform/databases) and read `DATABASE_URL` from runtime config or `process.env`. See [Connect an app](/platform/databases/connect). ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Scaling](/create/scaling): The SSR policy and scale to zero. - [Builds](/platform/deployments/builds): What the generated image does with your code. --- # Deploy SvelteKit SvelteKit is detected from its dependency and `svelte.config.js`, and runs as a Node.js container on port `3000`. Use `@sveltejs/adapter-node` (or leave `adapter-auto`, which picks it in a Node environment); the [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) runs the built server on Node 22. ## How it runs SvelteKit is a server-rendered frontend, so it runs under [the SSR policy](/create/scaling#the-ssr-policy): pinned to the **Micro** size, scaling 0-3, in a tier-1 region, always behind the edge CDN. - **Build.** `npm run build` writes the server to `build/`; the container starts it with Node. - **Port.** The Node adapter reads `PORT`, which is set for you. - **Prerendered pages** inside the app are served by the same server - nothing extra to configure. ## Configuration - Server code (`+page.server.ts`, hooks, endpoints) reads [environment variables](/platform/environment-variables) at runtime via `$env/dynamic/private`. - Prefer `$env/dynamic/*` over `$env/static/*` on the container: static values are inlined at image build, which runs [without your runtime variables](/platform/deployments/builds#environment-variables-at-build-time). ## Static instead? A fully prerendered site (`adapter-static`) is a plain [Svelte static build](/platform/frameworks/svelte) - CDN, bandwidth included, no server. Pick it when nothing renders per request. ## Database Provision [managed PostgreSQL or MySQL](/platform/databases) and read `DATABASE_URL` with `$env/dynamic/private`. See [Connect an app](/platform/databases/connect). ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Scaling](/create/scaling): The SSR policy and scale to zero. - [Svelte](/platform/frameworks/svelte): The static path for prerendered sites. --- # Deploy Remix Remix is detected from its dependencies and config, and runs as a Node.js container on port `3000`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) builds the app and runs `remix-serve` (or your `start` script) on Node 22; a repo with its own server file or Dockerfile is used as written. ## How it runs Remix is a server-rendered frontend, so it runs under [the SSR policy](/create/scaling#the-ssr-policy): pinned to the **Micro** size, scaling 0-3, in a tier-1 region, always behind the edge CDN. Loaders and actions execute on the origin; the edge serves what can be cached. - **Build.** `npm run build` produces the server and client bundles. - **Port.** `remix-serve` and the common Express setups respect `PORT`, which is set for you. ## Configuration Loaders and actions read [environment variables](/platform/environment-variables) from `process.env` at runtime, per environment. Anything that must reach the browser goes through a loader - Remix has no build-time public prefix, which fits the container rule: images are [built without your runtime variables](/platform/deployments/builds#environment-variables-at-build-time). ## Database Provision [managed PostgreSQL or MySQL](/platform/databases) and read `DATABASE_URL` in your loaders' data layer. See [Connect an app](/platform/databases/connect). ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Scaling](/create/scaling): The SSR policy and scale to zero. - [Builds](/platform/deployments/builds): What the generated image does with your code. --- # Deploy Astro Astro is detected from its dependency and `astro.config.mjs`. By default Astro builds static output - and that is how it deploys here: `npm run build` fills `dist/`, and `dist/` is served from the CDN with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Static (the default) - **Output directory:** `dist` - change it in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Environment variables:** static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so `PUBLIC_*` values inline as usual. Never put secrets behind the public prefix. - Islands, view transitions, and client scripts are all just files in the output - nothing special to configure. ## Server mode With `output: 'server'` and the Node adapter, Astro renders per request and runs as a Node.js container. Server-rendered frontends run under [the SSR policy](/create/scaling#the-ssr-policy): pinned to the **Micro** size, scaling 0-3, behind the edge CDN. - The Node adapter respects `PORT`, which is set for you. - Server code reads [environment variables](/platform/environment-variables) at runtime; the image build itself runs [without them](/platform/deployments/builds#environment-variables-at-build-time). ## Which to pick Content sites, blogs, docs, marketing: **static**. Per-request rendering, form actions, sessions: **server**. Astro's hybrid rendering counts as server mode - it needs the origin. ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Build settings](/create/build-settings): Output directory and build command overrides. - [Scaling](/create/scaling): What the SSR policy pins and why. --- # Deploy Django Django is detected from your dependencies and runs as a Python container on port `8000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Bind `0.0.0.0` on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```bash gunicorn myproject.wsgi --bind 0.0.0.0:$PORT ``` ## Settings from the environment Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime - read them in `settings.py`: ```python import os SECRET_KEY = os.environ["SECRET_KEY"] DEBUG = os.environ.get("DEBUG", "false") == "true" ALLOWED_HOSTS = ["*"] # or the exact hosts you serve ``` ## Database Provision [managed PostgreSQL](/platform/databases) and read `DATABASE_URL` - `dj-database-url` turns it into the settings dict: ```python import dj_database_url DATABASES = {"default": dj_database_url.config(conn_max_age=60)} ``` Keep `sslmode=require` from the [connection string](/platform/databases/connect) as-is. ## Migrations Run them where the database is reachable. The image build has no runtime variables, so the startup command is the natural place: ```bash python manage.py migrate && gunicorn myproject.wsgi --bind 0.0.0.0:$PORT ``` Or run them from your laptop against the same `DATABASE_URL` - the [connection string works anywhere](/platform/databases/connect#connect-from-your-laptop). ## Static files A container serves everything, static files included. The usual pattern is WhiteNoise: `collectstatic` during the build, WhiteNoise serving the result. Heavy asset traffic can move to its own [static site](/platform/frameworks#frontend) later. ## Related - [Python](/platform/frameworks/python): Plain Python apps, and the runtime's defaults. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Concurrency for CPU-heavy views, cold starts. --- # Deploy Ruby on Rails Rails is detected from your `Gemfile` and runs as a Ruby container on port `3000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own (modern Rails ships one; it is used as written). ## Serve it Puma binds the routed [port](/create/build-settings#port) via `PORT`, which is set for you - the stock `config/puma.rb` already reads it. Bind `0.0.0.0` (Puma's default in production). ## Configuration Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime: - `RAILS_ENV=production` and `SECRET_KEY_BASE` belong in the environment's variables. - `RAILS_LOG_TO_STDOUT=1` sends logs where [runtime logs](/platform/observability#runtime-logs) can see them. ## Database Rails reads `DATABASE_URL` natively - set it from the database's [Credentials tab](/platform/databases/connect) and `config/database.yml` needs no per-environment blocks. Point [previews](/platform/preview-environments) at a Shared-tier database by overriding the variable per environment. Migrations run where the database is reachable - the startup command works: ```bash bundle exec rails db:migrate && bundle exec puma -C config/puma.rb ``` ## Assets Precompile during the build so the image ships ready: ```bash bundle exec rails assets:precompile ``` Set it as part of the [build command](/create/build-settings#build-command) if your setup doesn't already run it. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Builds](/platform/deployments/builds): Generated images and bring-your-own Dockerfiles. - [Scaling](/create/scaling): Instances, concurrency, and cold starts. --- # Deploy Laravel Laravel is detected from `composer.json` and `artisan`, and runs as a PHP container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Configuration Everything that lives in a local `.env` moves to [environment variables](/platform/environment-variables) - Laravel reads them the same way, per environment: - `APP_KEY` - generate one (`php artisan key:generate --show`) and set it; the app won't boot without it. - `APP_ENV=production`, `APP_DEBUG=false`. - Deploying from [Claude or VS Code](/deploy-with-ai/overview)? Your local `.env` is read and set on the app for you. ## Database Provision [managed MySQL or PostgreSQL](/platform/databases) and set either `DATABASE_URL` or the discrete `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` - both shapes come straight off the [Credentials tab](/platform/databases/connect). Migrations run where the database is reachable - from the startup command or your laptop: ```bash php artisan migrate --force ``` ## Caches and logs - Build caches (`config:cache`, `route:cache`, `view:cache`) can run in the [build command](/create/build-settings#build-command) - they read no secrets. - Send logs to stderr (`LOG_CHANNEL=stderr`) so they land in [runtime logs](/platform/observability#runtime-logs). - Sessions and cache default to files, which don't survive instances coming and going - use the database or cookie drivers on an app that [scales](/create/scaling). ## Related - [Connect an app](/platform/databases/connect): Connection details in both shapes Laravel takes. - [Environment variables](/platform/environment-variables): Bulk-edit the whole set as a .env file. - [Symfony](/platform/frameworks/symfony): The other PHP framework, same container shape. --- # Deploy Symfony Symfony is detected from `composer.json` and runs as a PHP container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Configuration Symfony already prefers real environment variables over `.env` files in production - set them as [environment variables](/platform/environment-variables), per environment: - `APP_ENV=prod` and `APP_SECRET`. - Anything your bundles read (`MAILER_DSN`, `MESSENGER_TRANSPORT_DSN`, ...) - the bulk editor takes a whole `.env` paste. Containers receive variables at runtime; the image build runs [without them](/platform/deployments/builds#environment-variables-at-build-time), so `composer install` and cache warmup in the build must not depend on secrets. ## Database Doctrine reads `DATABASE_URL` natively - set it from the database's [Credentials tab](/platform/databases/connect). Run migrations where the database is reachable: ```bash php bin/console doctrine:migrations:migrate --no-interaction ``` ## Logs Log to stderr (Monolog's `php://stderr` handler) so entries land in [runtime logs](/platform/observability#runtime-logs). ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Laravel](/platform/frameworks/laravel): The other PHP framework, same container shape. - [Builds](/platform/deployments/builds): What the generated image does with your code. --- # Deploy WordPress WordPress runs as a PHP container on port `8080`, with the platform's tightest database integration: pick a [managed MySQL database](/platform/databases) in the app's settings and the `WORDPRESS_DB_HOST`, `WORDPRESS_DB_NAME`, `WORDPRESS_DB_USER`, and `WORDPRESS_DB_PASSWORD` variables are filled in for you. ## The fast path: the stack The [WordPress stack](/create/templates#wordpress) creates everything in one go - the app from the `light-cloud-com/starter-wordpress` starter, a Shared-tier MySQL database (1 GB), and the wiring between them. Deploy, open the URL, run the WordPress installer, done. ## Bring your own Deploying your own WordPress repository works the same way: 1. Create the app from your repository - detection recognises WordPress. 2. Create a [MySQL database](/create/database-settings) (Shared tier is fine to start). 3. Connect it in the app's settings - the `WORDPRESS_DB_*` variables appear - or set them by hand from the [Credentials tab](/platform/databases/connect). ## Things to know - **Keep it warm.** WordPress cold-starts slowly; the stack ships with [min instances](/create/scaling#min-instances) at `1`. Recommended for production - an always-on Micro is about $52 a month, on Starter and up; see [Pricing](/billing/pricing#containers). - **Uploads and plugins.** The filesystem inside a container does not survive deploys or scaling. Keep changes in the repository (themes, plugins via composer or committed files) and media on an offload plugin, not the local disk. - **One instance.** The stack pins max instances to `1`, which sidesteps shared-filesystem assumptions in plugins. Raise it only if your setup stores nothing locally. ## Related - [Templates](/create/templates): The WordPress stack, field by field. - [Databases](/platform/databases): Tiers, and when to move off the Shared tier. - [Scaling](/create/scaling): Warm minimums and what they cost. --- # Deploy Blazor Blazor is detected from your project files and runs as a .NET container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Kestrel must listen on the routed [port](/create/build-settings#port) on all interfaces. ASP.NET Core reads the standard variables - set nothing and the platform's `PORT` works via the generated setup, or be explicit: ``` ASPNETCORE_URLS=http://0.0.0.0:8080 ``` ## Configuration Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime, and ASP.NET Core's configuration system maps them as usual - `ConnectionStrings__Default` style double-underscore keys included. `ASPNETCORE_ENVIRONMENT=Production` is the conventional baseline. ## Database Provision [managed PostgreSQL or MySQL](/platform/databases) and feed the connection details to EF Core or your data layer from the environment - the [Credentials tab](/platform/databases/connect) has every field. Run EF migrations at startup or from your laptop against the same endpoint. ## Which Blazor? - **Blazor Server / interactive server rendering** - this page: a running container, under normal [scaling](/create/scaling). Server interactivity holds a connection per client, so size [concurrency](/create/scaling#concurrency) accordingly. - **Blazor WebAssembly, standalone** - a static build: publish output is files, served from the CDN. Deploy it as a [static site](/platform/frameworks#frontend) with the output directory pointed at the published `wwwroot`. ## Related - [Frameworks](/platform/frameworks): Everything supported and how detection works. - [Connect an app](/platform/databases/connect): Connection details for EF Core. - [Scaling](/create/scaling): Concurrency for connection-holding apps. --- # Deploy Wasp Wasp is detected from its project files and runs as a Node.js container on port `8080`. It is also the framework behind the [Open SaaS template](/create/templates#open-saas), which is the fastest way to a running Wasp app. ## What the platform handles Created from the Open SaaS stack, the fiddly parts are done for you: - A [PostgreSQL database](/platform/databases) on the Shared tier, wired in as `DATABASE_URL`. - A strong `JWT_SECRET`, generated at create time. - `WASP_SERVER_URL` and `WASP_WEB_CLIENT_URL`, derived from the app's public URL on every deploy - they track [custom domains](/platform/custom-domains) too. ## Bring your own Wasp app Deploying your own repository, set the same pieces as [environment variables](/platform/environment-variables): | Variable | Value | | --- | --- | | `DATABASE_URL` | From the database's [Credentials tab](/platform/databases/connect) | | `JWT_SECRET` | A long random string | | `WASP_SERVER_URL` | The app's public URL | | `WASP_WEB_CLIENT_URL` | The app's public URL | Wasp's server reads `PORT`, which is set for you. Migrations (`wasp db migrate-dev` output via Prisma) run where the database is reachable - startup or laptop, see [Connect an app](/platform/databases/connect#connect-from-your-laptop). ## Related - [Templates](/create/templates): The Open SaaS stack and its variable checklist. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Custom domains](/platform/custom-domains): The URL the derived variables follow. --- # Deploy React A React single-page app is detected from its dependencies and build config (`vite.config.ts` for Vite, `react-scripts` for Create React App) and deploys as a static site: `npm run build` fills the output directory, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build | Setup | Output directory | | --- | --- | | Vite | `dist` | | Create React App | `build` | Detection knows both; override in [Build settings](/create/build-settings#output-directory) if your config moves it. The install step follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so build-time inlining works as usual: `VITE_*` (Vite) or `REACT_APP_*` (CRA). Each [environment](/platform/preview-environments) builds with its own values - a preview can point at a staging API while production uses the real one. > [!WARNING] > Inlined values ship to every browser. API base URLs and public keys are fine; secrets are not - those belong on the server side of whatever API the app calls. ## Need a server? The React app itself needs none. When the backend is yours too, deploy it as its own app - [Express, FastAPI, Go, and friends](/platform/frameworks#backend) - and keep both in one repository with two [root directories](/create/application-settings#root-directory). Server-rendered React is [Next.js](/platform/frameworks/nextjs) or [Remix](/platform/frameworks/remix). ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Environment variables](/platform/environment-variables): Per-environment values at build time. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Vue A Vue single-page app is detected from its dependencies and `vite.config.ts`, and deploys as a static site: `npm run build` fills `dist/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `dist` - override in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so `VITE_*` values inline as usual, per [environment](/platform/preview-environments) - a preview can point at a staging API while production uses the real one. > [!WARNING] > Inlined values ship to every browser - never put secrets behind `VITE_`. ## Need a server? Deploy the API as its own [backend app](/platform/frameworks#backend) and keep both in one repository with two [root directories](/create/application-settings#root-directory). Server-rendered Vue is [Nuxt](/platform/frameworks/nuxt). ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Nuxt](/platform/frameworks/nuxt): The server-rendered path for Vue. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Angular Angular is detected from `angular.json`, and deploys as a static site: `npm run build` fills `dist/browser`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `dist/browser` - the modern application builder's default. Detection parses `angular.json` to find where your configuration actually writes; override in [Build settings](/create/build-settings#output-directory) if needed. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Per-environment configuration Angular bakes configuration at build time through its `environment.ts` file replacements rather than environment variables - `production` builds get `environment.prod.ts`. Values that differ per deployed [environment](/platform/preview-environments) (API base URLs, keys) can also be read from variables inside `angular.json`-driven builds only via custom tooling; the simplest platform-native pattern is one build configuration per case, selected in the [build command](/create/build-settings#build-command): ```bash npm run build -- --configuration=staging ``` Set a different build command on the preview environment than on production. ## Need a server? Deploy the API as its own [backend app](/platform/frameworks#backend) - keep both in one repository with two [root directories](/create/application-settings#root-directory). ## Related - [Build settings](/create/build-settings): Per-environment build command overrides. - [Preview environments](/platform/preview-environments): Different settings per branch. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Svelte A Svelte single-page app (Vite-based, no SvelteKit server) is detected from its dependencies and deploys as a static site: `npm run build` fills `dist/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `dist` - override in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so `VITE_*` values inline as usual, per [environment](/platform/preview-environments). > [!WARNING] > Inlined values ship to every browser - never put secrets behind `VITE_`. ## SvelteKit instead? Routing, server load functions, or form actions mean [SvelteKit](/platform/frameworks/sveltekit) - a container under the SSR policy, or `adapter-static` back to the CDN. ## Related - [SvelteKit](/platform/frameworks/sveltekit): The full-framework path for Svelte. - [Build settings](/create/build-settings): Install, build, and output configuration. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy SolidJS A SolidJS app is detected from its dependencies and `vite.config.ts`, and deploys as a static site: `npm run build` fills `dist/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `dist` - override in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so `VITE_*` values inline as usual, per [environment](/platform/preview-environments). > [!WARNING] > Inlined values ship to every browser - never put secrets behind `VITE_`. ## Need a server? Deploy the API as its own [backend app](/platform/frameworks#backend) and keep both in one repository with two [root directories](/create/application-settings#root-directory). ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Environment variables](/platform/environment-variables): Per-environment values at build time. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Qwik Qwik is detected from its dependencies and Vite config, and deploys as a static site: `npm run build` fills `dist/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). Qwik's resumability works from plain static files - the HTML carries the serialized state and hydrates nothing. ## Build - **Output directory:** `dist` - override in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime). Qwik inlines `PUBLIC_*` values into the client build, per [environment](/platform/preview-environments). > [!WARNING] > Inlined values ship to every browser - never put secrets behind `PUBLIC_`. ## Server-side Qwik City? Endpoints and server-rendered routes need a running server - deploy that shape as a Node container via your own start setup, or keep the site static and put the API in its own [backend app](/platform/frameworks#backend). ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Environment variables](/platform/environment-variables): Per-environment values at build time. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Gatsby Gatsby is detected from its dependency and `gatsby-config`, and deploys as a static site: `npm run build` fills `public/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `public` - Gatsby's fixed output. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). - Source plugins that pull from CMSs and APIs run during the build, so their credentials must be available then - which they are: static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime). ## Environment variables `GATSBY_*` values inline into the browser bundle; unprefixed variables stay build-side (`gatsby-node`, source plugins), per [environment](/platform/preview-environments) - a preview can build against a staging CMS space. > [!WARNING] > `GATSBY_*` values ship to every browser - CMS delivery tokens for public content are usually fine, management tokens never are. ## Rebuilding on content changes Builds trigger on push. Content edited in a headless CMS becomes visible on the next deploy - push a commit or press **Redeploy** on the environment. See [Rollbacks and redeploys](/platform/deployments/rollbacks#redeploy). ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Environment variables](/platform/environment-variables): Build-time values per environment. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Docusaurus Docusaurus is detected from its dependency and `docusaurus.config.js`, and deploys as a static site: `npm run build` fills `build/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). The usual docs-site setup - versioned docs, blog, search plugins - is all just build output. ## Build - **Output directory:** `build` - override in [Build settings](/create/build-settings#output-directory) if your config moves it. - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). - Set `url` in `docusaurus.config.js` to your [custom domain](/platform/custom-domains) so canonical URLs and sitemaps come out right. ## Previews for docs reviews Every branch gets its own [preview environment](/platform/preview-environments) - a docs change can be reviewed rendered, at its own URL, before it merges. This is the whole preview workflow at its most useful. ## Broken links fail loudly Docusaurus checks internal links during the build; a broken link can fail it. That failure lands in the [build logs](/platform/observability#build-logs) with the offending lines extracted - fix and push. ## Related - [Preview environments](/platform/preview-environments): A rendered preview per branch. - [Build settings](/create/build-settings): Install, build, and output configuration. - [Custom domains](/platform/custom-domains): Your docs on your own hostname. --- # Deploy Eleventy Eleventy is detected from its dependency and config file, and deploys as a static site: `npm run build` fills `_site/`, and the CDN serves it with [bandwidth included](/billing/pricing#static-sites). No server, no region, no cold starts. ## Build - **Output directory:** `_site` - Eleventy's default; override in [Build settings](/create/build-settings#output-directory) if your config sets `dir.output` elsewhere. - **Build command:** whatever your `build` script runs (`eleventy`, or `eleventy && postcss ...` chains). - **Install:** follows your lockfile - see [How detection works](/platform/frameworks#how-detection-works). ## Environment variables Eleventy configs and data files are plain Node - read `process.env` freely at build time. Static builds run [with the environment's variables set](/platform/environment-variables#build-time-and-runtime), so data files can fetch from APIs with per-[environment](/platform/preview-environments) credentials. Values used only at build time never reach the browser unless your templates print them - keep secrets out of rendered output. ## Related - [Build settings](/create/build-settings): Install, build, and output configuration. - [Preview environments](/platform/preview-environments): A rendered preview per branch. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy static HTML No framework, no build - a repository (or [upload](/create/upload)) of HTML, CSS, and JavaScript deploys as-is. The output directory is the repository root, and its contents are exactly what the CDN serves, with [bandwidth included](/billing/pricing#static-sites). ## Settings - **Build command:** empty. Add one if you do have a step (a Tailwind CLI pass, an esbuild bundle) - then point the [output directory](/create/build-settings#output-directory) at what it writes. - **Output directory:** `.` by default. Set it to a subfolder (`site/`, `dist/`) to serve only that folder - nothing outside it is deployed. ## This is also the escape hatch Generators the platform doesn't build yet - Hugo, Jekyll, anything bespoke - deploy this way: build locally, commit or upload the output, serve it as static HTML. See the note on [Frameworks](/platform/frameworks#frontend). ## What you still get Everything except a build: [preview environments](/platform/preview-environments) per branch, [custom domains](/platform/custom-domains) with automatic certificates, deploy history, and per-environment variables (useful only if a build step consumes them - plain files inline nothing). ## Related - [Build settings](/create/build-settings): Adding a build step in front of static output. - [Deploy from an upload](/create/upload): Shipping a folder without a repository. - [Custom domains](/platform/custom-domains): Your own hostname on the CDN. --- # Deploy Express Express is detected from your `package.json` and runs as a Node.js container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) on Node 22, installing with your lockfile's package manager. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```js const app = require('express')(); app.listen(process.env.PORT || 8080, () => { console.log('up'); }); ``` `app.listen` binds all interfaces by default - correct for containers. Log with `console.log`/`console.error`; both land in [runtime logs](/platform/observability#runtime-logs). ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) from `process.env` at startup. For a database, set `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) - pg, mysql2, Prisma, and Drizzle all take it. Connect lazily so [cold starts](/create/scaling#cold-starts) stay short. ## Scaling Defaults fit a typical I/O-bound API: [concurrency](/create/scaling#concurrency) 80, [scale to zero](/create/scaling#min-instances) when idle. Set min instances to `1` on production if the first request after quiet must be fast. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Builds](/platform/deployments/builds): The generated Node image, step by step. - [Scaling](/create/scaling): Instances, concurrency, CPU target. --- # Deploy Fastify Fastify is detected from your `package.json` and runs as a Node.js container on port `3000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) on Node 22. ## Serve it - and bind the host Fastify's default host is `127.0.0.1`, which is unreachable from outside a container. Bind `0.0.0.0` explicitly, on the routed [port](/create/build-settings#port): ```js const fastify = require('fastify')({ logger: true }); fastify.listen({ port: Number(process.env.PORT) || 3000, host: '0.0.0.0' }); ``` A green deploy that never answers is almost always this bind - see [When a build fails](/platform/deployments/builds#when-a-build-fails). ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) from `process.env` at startup. Set `DATABASE_URL` from the [Credentials tab](/platform/databases/connect); Fastify's logger writes JSON to stdout, which [runtime logs](/platform/observability#runtime-logs) pick up as-is. ## Scaling Fastify's throughput pairs well with the default [concurrency](/create/scaling#concurrency) of 80 or higher; [scale to zero](/create/scaling#min-instances) covers idle time. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Builds](/platform/deployments/builds): The generated Node image, step by step. - [Scaling](/create/scaling): Instances, concurrency, CPU target. --- # Deploy NestJS NestJS is detected from your `package.json` and runs as a Node.js container on port `3000`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) runs your `build` script (Nest's TypeScript compile) and starts the compiled app on Node 22. ## Serve it Listen on the routed [port](/create/build-settings#port), all interfaces: ```ts const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000, '0.0.0.0'); ``` ## Configuration and database `@nestjs/config` reads `process.env` - which is where [environment variables](/platform/environment-variables#build-time-and-runtime) arrive at runtime, per environment. Set `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) for TypeORM, Prisma, or Mikro-ORM. Run migrations at startup or from your laptop against the same endpoint - see [Connect from your laptop](/platform/databases/connect#connect-from-your-laptop). ## Scaling Nest apps often carry DI-heavy startup - keep providers lazy where possible so [cold starts](/create/scaling#cold-starts) stay short, or set [min instances](/create/scaling#min-instances) to `1` on production. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Builds](/platform/deployments/builds): The generated Node image, step by step. - [Scaling](/create/scaling): Warm minimums and cold starts. --- # Deploy AdonisJS AdonisJS is detected from your `package.json` and runs as a Node.js container on port `3333`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) runs your `build` script and starts the compiled server from `build/` on Node 22. ## Serve it Adonis reads `HOST` and `PORT` from the environment. `PORT` is set for you; set `HOST` in the app's [environment variables](/platform/environment-variables): ``` HOST=0.0.0.0 APP_KEY= NODE_ENV=production ``` `APP_KEY` (from `node ace generate:key`) is required in production. ## Database Set `DATABASE_URL`, or Lucid's discrete `DB_*` variables, from the [Credentials tab](/platform/databases/connect). Migrations run where the database is reachable: ```bash node ace migration:run --force ``` ## Scaling Defaults fit a typical API - [concurrency](/create/scaling#concurrency) 80, [scale to zero](/create/scaling#min-instances) when idle. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Environment variables](/platform/environment-variables): Where HOST, PORT, and APP_KEY live. - [Builds](/platform/deployments/builds): The generated Node image, step by step. --- # Deploy Hono Hono is detected from your `package.json` and runs as a Node.js container on port `3000`, via its Node adapter. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) on Node 22. ## Serve it Use `@hono/node-server` and the routed [port](/create/build-settings#port): ```ts import { serve } from '@hono/node-server'; import { Hono } from 'hono'; const app = new Hono(); app.get('/', c => c.text('up')); serve({ fetch: app.fetch, port: Number(process.env.PORT) || 3000 }); ``` The adapter binds all interfaces - nothing else to configure. ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) from `process.env` at startup. Set `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) for your driver or ORM. ## Scaling Hono starts fast, which keeps [cold starts](/create/scaling#cold-starts) cheap - a good fit for [scale to zero](/create/scaling#min-instances) everywhere, previews and production alike. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Builds](/platform/deployments/builds): The generated Node image, step by step. - [Scaling](/create/scaling): Instances, concurrency, CPU target. --- # Deploy Node.js A plain Node server - `http.createServer`, a WebSocket service, a worker with an HTTP health endpoint - deploys without any framework. Detection reads `package.json`, the [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) runs on Node 22, and the default port is `8080`. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```js const http = require('http'); http .createServer((req, res) => res.end('up')) .listen(process.env.PORT || 8080); ``` `server.listen(port)` binds all interfaces by default. Write logs to stdout/stderr for [runtime logs](/platform/observability#runtime-logs). ## Build and start - **Install** follows your lockfile (`pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`) - exact installs with a lockfile present. - A `build` script (TypeScript compile, bundling) runs during the image build via the [build command](/create/build-settings#build-command). - The container starts your `start` script. ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) from `process.env` at startup - never bake them into the build. `DATABASE_URL` comes from the [Credentials tab](/platform/databases/connect). ## Related - [Builds](/platform/deployments/builds): The generated Node image, step by step. - [Dockerfile](/platform/frameworks/docker): Take over the image entirely. - [Scaling](/create/scaling): Instances, concurrency, CPU target. --- # Deploy FastAPI FastAPI is detected from your dependencies and runs as a Python container on port `8000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Bind `0.0.0.0` on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```bash uvicorn app.main:app --host 0.0.0.0 --port $PORT ``` ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) at startup - `os.environ`, or pydantic-settings, which maps them to a settings model. For the database, set `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) - SQLAlchemy and the async drivers take the URL directly; keep `sslmode=require` in place. Run Alembic migrations at startup or from your laptop against the same endpoint - see [Connect from your laptop](/platform/databases/connect#connect-from-your-laptop). ## Scaling Async I/O suits the default [concurrency](/create/scaling#concurrency) of 80. CPU-bound endpoints (ML inference, heavy serialization) want it lower - `10`-`20` - or a bigger [size](/create/application-settings#size). [Scale to zero](/create/scaling#min-instances) covers idle time; connect to the database lazily so cold starts stay short. ## Related - [Python](/platform/frameworks/python): The runtime's defaults for any Python app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Concurrency for CPU-bound endpoints. --- # Deploy Flask Flask is detected from your dependencies and runs as a Python container on port `8000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Run a production server, not `flask run` - bind `0.0.0.0` on the routed [port](/create/build-settings#port): ```bash gunicorn "app:create_app()" --bind 0.0.0.0:$PORT ``` (`app:app` for a module-level app object.) ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) at startup: ```python import os app.config["SECRET_KEY"] = os.environ["SECRET_KEY"] app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"] ``` `DATABASE_URL` comes from the [Credentials tab](/platform/databases/connect); keep `sslmode=require` in place. Flask-Migrate/Alembic runs where the database is reachable - startup command or laptop. ## Scaling Sync Flask workers hold a request each - lower [concurrency](/create/scaling#concurrency) (`10`-`20`) matches a small Gunicorn worker count better than the default 80. [Scale to zero](/create/scaling#min-instances) covers idle time. ## Related - [Python](/platform/frameworks/python): The runtime's defaults for any Python app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Matching concurrency to worker counts. --- # Deploy Python Any Python web app runs as a container on the Python runtime - the same machinery behind the [Django](/platform/frameworks/django), [FastAPI](/platform/frameworks/fastapi), and [Flask](/platform/frameworks/flask) guides, available to every other Python server too. Detection reads your dependency manifest; the default port is `8000`. ## The contract - **Bind `0.0.0.0`**, not `127.0.0.1` - traffic is routed into the container from outside. - **Listen on the routed [port](/create/build-settings#port)**, or read `PORT` from the environment, which is set to it. - **Log to stdout/stderr** - that is what lands in [runtime logs](/platform/observability#runtime-logs). ```bash uvicorn app:app --host 0.0.0.0 --port $PORT # ASGI gunicorn app:app --bind 0.0.0.0:$PORT # WSGI ``` ## Configuration and database Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime - read `os.environ` at startup, never at image build. `DATABASE_URL` comes from the [Credentials tab](/platform/databases/connect); SQLAlchemy and most drivers take it directly. ## Scale to zero and workers Python containers [scale to zero](/create/scaling#min-instances) like any other. Keep startup light, connect to the database lazily, and match [concurrency](/create/scaling#concurrency) to your server model - high for async, `10`-`20` for sync workers or CPU-heavy endpoints. ## Related - [Django](/platform/frameworks/django): Settings, migrations, and static files. - [FastAPI](/platform/frameworks/fastapi): Uvicorn and async concurrency. - [Flask](/platform/frameworks/flask): Gunicorn and sync worker sizing. - [Dockerfile](/platform/frameworks/docker): Take over the image when you need system packages. --- # Deploy Gin Gin is detected from your `go.mod` and runs as a Go container on port `8080`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) compiles the module and ships the binary. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```go r := gin.Default() r.GET("/", func(c *gin.Context) { c.String(200, "up") }) port := os.Getenv("PORT") if port == "" { port = "8080" } r.Run(":" + port) // binds 0.0.0.0 ``` ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `os.Getenv` at startup. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) works with `pgx`, `database/sql`, and GORM; keep `sslmode=require`. ## Scaling A compiled Go binary cold-starts in well under a second - Gin apps are the happiest case for [scale to zero](/create/scaling#min-instances). Go's scheduler handles high [concurrency](/create/scaling#concurrency) comfortably; the per-size caps are the practical limit. ## Related - [Go](/platform/frameworks/go): The runtime's defaults for any Go app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Concurrency caps per size. --- # Deploy Echo Echo is detected from your `go.mod` and runs as a Go container on port `8080`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) compiles the module and ships the binary. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```go e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(200, "up") }) port := os.Getenv("PORT") if port == "" { port = "8080" } e.Logger.Fatal(e.Start(":" + port)) // binds 0.0.0.0 ``` ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `os.Getenv` at startup. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) works with `pgx`, `database/sql`, and GORM; keep `sslmode=require`. ## Scaling Compiled binaries cold-start in well under a second - a natural fit for [scale to zero](/create/scaling#min-instances), with [concurrency](/create/scaling#concurrency) limited mostly by the per-size caps. ## Related - [Go](/platform/frameworks/go): The runtime's defaults for any Go app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Concurrency caps per size. --- # Deploy Fiber Fiber is detected from your `go.mod` and runs as a Go container on port `8080`. The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) compiles the module and ships the binary. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```go app := fiber.New() app.Get("/", func(c *fiber.Ctx) error { return c.SendString("up") }) port := os.Getenv("PORT") if port == "" { port = "8080" } log.Fatal(app.Listen(":" + port)) // binds 0.0.0.0 ``` ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `os.Getenv` at startup. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) works with `pgx`, `database/sql`, and GORM; keep `sslmode=require`. ## Scaling Compiled binaries cold-start in well under a second - a natural fit for [scale to zero](/create/scaling#min-instances). Fiber's fasthttp core thrives at high [concurrency](/create/scaling#concurrency); the per-size caps are the practical limit. ## Related - [Go](/platform/frameworks/go): The runtime's defaults for any Go app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Concurrency caps per size. --- # Deploy Go Any Go module deploys without a framework - plain `net/http` included. Detection reads your `go.mod`, the [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) compiles the module and ships the binary, and the default port is `8080`. ## Serve it Listen on the routed [port](/create/build-settings#port) - `PORT` is set to it: ```go port := os.Getenv("PORT") if port == "" { port = "8080" } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("up")) }) log.Fatal(http.ListenAndServe(":"+port, nil)) // binds 0.0.0.0 ``` Log with the standard library or slog to stdout/stderr - both land in [runtime logs](/platform/observability#runtime-logs). ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `os.Getenv` at startup. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) works with `pgx` and `database/sql`; keep `sslmode=require`. ## Why Go loves this platform A compiled binary cold-starts in well under a second, so [scale to zero](/create/scaling#min-instances) is close to free - previews and low-traffic APIs cost nothing while idle and answer promptly when hit. ## Related - [Gin](/platform/frameworks/gin): The framework guides: Gin, Echo, Fiber. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Dockerfile](/platform/frameworks/docker): Take over the image for cgo or system packages. --- # Deploy Spring Boot Spring Boot is detected from your `pom.xml` or `build.gradle` and runs as a Java container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Match the routed [port](/create/build-settings#port), either by leaving both at `8080`, or by mapping the `PORT` variable in `application.properties`: ```properties server.port=${PORT:8080} ``` Tomcat binds all interfaces by default - nothing else to configure. ## Configuration and database Spring's relaxed binding maps [environment variables](/platform/environment-variables#build-time-and-runtime) onto properties - `SPRING_DATASOURCE_URL` becomes `spring.datasource.url`. From the database's [Credentials tab](/platform/databases/connect): ``` SPRING_DATASOURCE_URL=jdbc:postgresql://203.0.113.10:5432/mydb?sslmode=require SPRING_DATASOURCE_USERNAME=appuser SPRING_DATASOURCE_PASSWORD=... ``` Flyway and Liquibase run their migrations at startup by default, where the database is reachable - which fits the platform's model. ## Scaling JVM startup is the slow end of [cold starts](/create/scaling#cold-starts) - seconds, not milliseconds. For production, set [min instances](/create/scaling#min-instances) to `1`; previews can stay at zero. Memory matters too: pick at least the [Small size](/create/application-settings#size) for a typical Boot app. ## Related - [Java](/platform/frameworks/java): The runtime's defaults for any JVM app. - [Connect an app](/platform/databases/connect): Connection details in JDBC shape. - [Scaling](/create/scaling): Warm minimums for JVM apps. --- # Deploy Quarkus Quarkus is detected from your `pom.xml` or `build.gradle` and runs as a Java container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own (Quarkus projects often ship several under `src/main/docker` - a repo-root `Dockerfile` is the one that counts). ## Serve it Match the routed [port](/create/build-settings#port) - leave both at `8080`, or map the variable: ```properties quarkus.http.port=${PORT:8080} quarkus.http.host=0.0.0.0 ``` `0.0.0.0` is Quarkus's default host in a container profile. ## Configuration and database MicroProfile Config reads [environment variables](/platform/environment-variables#build-time-and-runtime) - `QUARKUS_DATASOURCE_JDBC_URL` maps to `quarkus.datasource.jdbc.url`. From the [Credentials tab](/platform/databases/connect): ``` QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://203.0.113.10:5432/mydb?sslmode=require QUARKUS_DATASOURCE_USERNAME=appuser QUARKUS_DATASOURCE_PASSWORD=... ``` Flyway/Liquibase migrations at startup work as usual. ## Scaling Quarkus starts fast for a JVM framework, and a native build starts in tens of milliseconds - native images pair beautifully with [scale to zero](/create/scaling#min-instances). Building native happens in your own Dockerfile; see [Deploy with a Dockerfile](/platform/frameworks/docker). ## Related - [Java](/platform/frameworks/java): The runtime's defaults for any JVM app. - [Dockerfile](/platform/frameworks/docker): Ship a native image for instant cold starts. - [Connect an app](/platform/databases/connect): Connection details in JDBC shape. --- # Deploy Java Any JVM web app runs as a container on the Java runtime - the machinery behind the [Spring Boot](/platform/frameworks/spring-boot) and [Quarkus](/platform/frameworks/quarkus) guides, available to Micronaut, Javalin, Vert.x, Ktor, and plain servlet apps too. Detection reads your Maven or Gradle build; the default port is `8080`. ## The contract - Listen on the routed [port](/create/build-settings#port), or read the `PORT` variable, which is set to it. - Bind `0.0.0.0` - a loopback bind is unreachable from outside the container. - Log to stdout/stderr for [runtime logs](/platform/observability#runtime-logs). ## Configuration and database Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime - read them with `System.getenv` or your framework's config layer. JDBC details come from the database's [Credentials tab](/platform/databases/connect): ``` jdbc:postgresql://203.0.113.10:5432/mydb?sslmode=require ``` ## Scaling JVM cold starts run seconds - keep production at [min instances](/create/scaling#min-instances) `1` and give the heap room with at least the [Small size](/create/application-settings#size). Previews stay happily at zero. ## Related - [Spring Boot](/platform/frameworks/spring-boot): Relaxed binding and datasource variables. - [Quarkus](/platform/frameworks/quarkus): Fast JVM startup, native option. - [Dockerfile](/platform/frameworks/docker): Custom JDKs, jlink images, native builds. --- # Deploy Sinatra Sinatra is detected from your `Gemfile` and runs as a Ruby container on port `3000`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Run a real server (Puma via `config.ru` is the standard), bound to all interfaces on the routed [port](/create/build-settings#port): ```bash bundle exec puma -b tcp://0.0.0.0:$PORT ``` Or inside a classic-style app: ```ruby set :bind, '0.0.0.0' set :port, ENV.fetch('PORT', 3000) ``` ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `ENV` at startup. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) plugs into Sequel or ActiveRecord directly. ## Scaling Sinatra apps are small and boot fast - a good fit for [scale to zero](/create/scaling#min-instances). Sync Ruby workers hold a request each, so keep [concurrency](/create/scaling#concurrency) near your Puma thread count rather than the default 80. ## Related - [Ruby](/platform/frameworks/ruby): The runtime's defaults for any Rack app. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. - [Scaling](/create/scaling): Matching concurrency to server threads. --- # Deploy Ruby Any Rack-compatible Ruby app runs as a container on the Ruby runtime - the machinery behind the [Rails](/platform/frameworks/rails) and [Sinatra](/platform/frameworks/sinatra) guides, available to Hanami, Roda, Grape, and plain Rack apps too. Detection reads your `Gemfile`; the default port is `3000`. ## The contract - Serve with a production server (Puma is the default answer), bound to `0.0.0.0` on the routed [port](/create/build-settings#port) - `PORT` is set to it. - Log to stdout/stderr for [runtime logs](/platform/observability#runtime-logs). ```bash bundle exec puma -b tcp://0.0.0.0:$PORT ``` ## Configuration and database Containers receive [environment variables](/platform/environment-variables#build-time-and-runtime) at runtime - read `ENV` at boot. `DATABASE_URL` from the [Credentials tab](/platform/databases/connect) is understood by ActiveRecord and Sequel natively. ## Scaling Sync Ruby servers hold a request per thread - set [concurrency](/create/scaling#concurrency) near your total thread count instead of the default 80, and let idle apps [scale to zero](/create/scaling#min-instances). ## Related - [Ruby on Rails](/platform/frameworks/rails): The full-framework guide. - [Sinatra](/platform/frameworks/sinatra): The small-framework guide. - [Connect an app](/platform/databases/connect): DATABASE_URL, TLS, per-environment databases. --- # Deploy ASP.NET ASP.NET Core is detected from your project files and runs as a .NET container on port `8080`. No Dockerfile needed - one is [generated](/platform/deployments/builds#dockerfiles-generated-or-your-own) unless the repo has its own. ## Serve it Kestrel must listen on the routed [port](/create/build-settings#port) on all interfaces. Modern .NET containers default to `8080`; being explicit never hurts: ``` ASPNETCORE_URLS=http://0.0.0.0:8080 ``` ## Configuration and database ASP.NET Core's configuration maps [environment variables](/platform/environment-variables#build-time-and-runtime) with double-underscore nesting - `ConnectionStrings__Default` reaches `ConnectionStrings:Default`. From the database's [Credentials tab](/platform/databases/connect): ``` ConnectionStrings__Default=Host=203.0.113.10;Port=5432;Database=mydb;Username=appuser;Password=...;SSL Mode=Require ``` EF Core migrations run at startup (`context.Database.Migrate()`) or from your machine against the same endpoint. ## Scaling .NET cold starts sit between Go and the JVM - ReadyToRun builds shorten them further. [Scale to zero](/create/scaling#min-instances) works well for APIs; set a warm minimum on latency-sensitive production. ## Related - [Blazor](/platform/frameworks/blazor): The UI-framework guide on the same runtime. - [Connect an app](/platform/databases/connect): Connection details for EF Core. - [Scaling](/create/scaling): Warm minimums and cold starts. --- # Deploy PHP Any PHP app runs as a container on the PHP runtime - the machinery behind the [Laravel](/platform/frameworks/laravel), [Symfony](/platform/frameworks/symfony), and [WordPress](/platform/frameworks/wordpress) guides, available to plain PHP too. Detection reads your `composer.json` (or plain `.php` entry points); the default port is `8080`. ## How it runs The [generated image](/platform/deployments/builds#dockerfiles-generated-or-your-own) serves your app on the routed [port](/create/build-settings#port) with a production PHP setup - Composer dependencies installed during the build. A repo with its own Dockerfile (FrankenPHP, custom FPM tuning) is used as written. ## Configuration and database Read [environment variables](/platform/environment-variables#build-time-and-runtime) with `getenv()`/`$_ENV` at request time - they are present for every request, per environment. Database credentials come from the [Credentials tab](/platform/databases/connect) as a URL or discrete values, whichever your app expects. ## The stateless rule Instances come and go with [scaling](/create/scaling) - local files don't survive. Sessions belong in the database or cookies, uploads on object storage via your app's storage layer, caches somewhere shared. Frameworks handle this with a config switch; plain PHP needs it kept in mind. ## Related - [Laravel](/platform/frameworks/laravel): The full-framework guide. - [Symfony](/platform/frameworks/symfony): The other full-framework guide. - [WordPress](/platform/frameworks/wordpress): The CMS path with one-click database wiring. --- # Deploy with a Dockerfile A `Dockerfile` in the repository **always wins**: detection steps aside and the app builds and runs exactly as written, whatever is inside. This is the escape hatch for unlisted frameworks, unusual toolchains, and anything else that runs in a container. ## The contract Three things the container must do: 1. **Listen on the routed port.** Traffic is routed to the [port setting](/create/build-settings#port) (default `8080` for Dockerfile apps). The same value arrives as the `PORT` environment variable - reading it is the most portable choice. 2. **Bind 0.0.0.0.** A server bound to `127.0.0.1` is unreachable from outside the container. 3. **Log to stdout and stderr.** That is what lands in [runtime logs](/platform/observability#runtime-logs). Configuration arrives as [environment variables at runtime](/platform/deployments/builds#environment-variables-at-build-time) - the image build itself runs without them, so don't bake secrets or config into layers. ## An example A small multi-stage Node image: ```dockerfile FROM node:22-slim AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-slim WORKDIR /app ENV NODE_ENV=production COPY --from=build /app/dist ./dist COPY package*.json ./ RUN npm ci --omit=dev EXPOSE 8080 CMD ["node", "dist/server.js"] ``` The same pattern works for Go (`scratch` or `distroless` final stage), Rust, JVM apps, and anything else: build in one stage, run from a minimal one. ## Keep the image lean Image size is startup time. An app that [scales to zero](/create/scaling#min-instances) pays its image's boot cost on the first request after idle: - Multi-stage builds - compilers and dev dependencies stay in the build stage. - Slim or distroless base images. - A `.dockerignore` that excludes `node_modules`, `.git`, local caches, and test data from the build context. ## Notes - `EXPOSE` is documentation - routing uses the port setting, not the EXPOSE line. Keep them matching for your own sanity. - The [build command](/create/build-settings#build-command), when set, runs during the image build. - Scaling settings apply as usual: [instances, concurrency, CPU target](/create/scaling). - A green build with an unreachable app is almost always a port mismatch or a `127.0.0.1` bind - see [When a build fails](/platform/deployments/builds#when-a-build-fails). ## Related - [Builds](/platform/deployments/builds): How images are built and what a generated Dockerfile does. - [Scaling](/create/scaling): Cold starts and why image size matters. - [Observability](/platform/observability): Where your container's logs end up. --- # Environment variables Set configuration values and secrets for an application, and override them per environment. ## Per app and per environment Variables defined on the application apply to every environment. Each environment can override them or add its own, and at deploy time the environment's value wins: | Defined on | Applies to | Wins when | | --- | --- | --- | | Application | Every environment | No environment override exists | | Environment | That environment only | Always | That is how production keeps the real `DATABASE_URL` while a [preview](/platform/preview-environments) points at a test database. In the console you edit variables on the environment's settings tab; anything inherited from the application is listed above the editable rows, so you can see the whole set an environment will run with. ## Edit as a .env file The variables card has two modes. **Form** mode is one row per variable. **Bulk edit** is a textarea that speaks `.env`: ```bash # comments and blank lines are fine DATABASE_URL=postgres://appuser:secret@203.0.113.10:5432/mydb API_KEY="sk-live-..." export NODE_ENV=production ``` Paste a `.env` file straight in - quoted values, `export` prefixes, and `KEY: value` lines all parse, and the pasted set replaces the current one. **Copy all** goes the other way and puts the whole set on your clipboard as `.env` text. When a save changes the set, the environment redeploys automatically, so the running service always matches what the console shows. ## Build time and runtime - **Static builds** run your build command with the variables set, so values that get inlined at build time - `NEXT_PUBLIC_*`, `VITE_*`, and friends - work as usual. - **Container apps** receive variables at runtime: read them from `process.env` (or your language's equivalent) when the server starts. The container image itself is built without them, so don't bake configuration into a container build - read it at startup. See [Builds](/platform/deployments/builds#environment-variables-at-build-time). - On containers, `PORT` is set for you (8080 by default) - listen on it and the routing works. Common build-time prefixes, for reference: | Framework | Inlined prefix | | --- | --- | | Next.js | `NEXT_PUBLIC_` | | Vite (React, Vue, Svelte, SolidJS, Qwik) | `VITE_` | | Create React App | `REACT_APP_` | | Gatsby | `GATSBY_` | | Astro | `PUBLIC_` | Anything inlined ships to the browser - never put secrets behind these prefixes. ## From a .env file, automatically When you deploy from the VS Code extension or the MCP server, variables in your local `.env` file are read and set on the app for you. See [Deploy with AI](/deploy-with-ai/overview). ## Database credentials Provision a [database](/platform/databases) and its credentials tab gives you a ready connection string - set it as `DATABASE_URL` on the environments that should use it. The full wiring guide is [Connect an app](/platform/databases/connect). WordPress apps can connect a database with one click instead, and [stacks](/create/templates#stacks) arrive pre-wired. ## History without leaks Variable changes show up in the environment's activity history - which keys were added, removed, or changed, by whom, and when. Values are never written to the history, so the audit trail is not a credential leak. > [!WARNING] > Variables are the right place for secrets; the repository is not. Keep keys out of code and out of uploaded archives - anything in a static build's output directory is served publicly. ## Related - [Connect an app](/platform/databases/connect): DATABASE_URL and per-environment databases. - [Builds](/platform/deployments/builds): When variables are present, when they are not. - [Preview environments](/platform/preview-environments): Overriding configuration per branch. --- # Observability Watch a build, then follow the running service, in the same place you deploy. Production and every preview keep their own logs and metrics, so you are always looking at the environment you mean to. ## Build logs Each deploy streams its progress step by step as it runs - installing dependencies, building, creating the container image, releasing. If a build fails, the lines that broke it are pulled out of the build output and put on the deploy record, so the reason is on screen without digging. The steps themselves are explained on [Builds](/platform/deployments/builds#the-steps). ## Runtime logs Container environments keep searchable runtime logs in the console: - Filter by time range (last hour to last 7 days), severity, or free-text search. - **Live tail** follows new entries as they arrive - useful while you reproduce a bug. - Request entries carry their latency, so slow endpoints stand out while you scroll. Anything your app writes to stdout or stderr lands here. Static sites are files on a CDN - nothing runs, so there are no runtime logs to read. ## Metrics Each container environment has a metrics dashboard with six charts: | Chart | Shows | | --- | --- | | CPU | Utilization of the allocated CPU, in percent | | Memory | Utilization of the allocated memory, in percent | | Requests | Request count over time | | Active instances | How many instances are serving - watch [scale to zero](/create/scaling#min-instances) work | | Bandwidth out | Bytes sent | | Bandwidth in | Bytes received | Charts refresh every 30 seconds while open, and history is kept for 90 days. The environment overview shows CPU and memory sparklines for the last hour, so a glance tells you whether anything is running hot. ## Reading the charts - **CPU pinned near 100%** - requests are queueing. Lower the [CPU target](/create/scaling#cpu-target), lower [concurrency](/create/scaling#concurrency), or move up a [size](/create/application-settings#size). - **Memory climbing between deploys** - likely a leak; each deploy resets it. Watch whether it climbs to a crash or plateaus. - **Active instances stuck at max** - the [ceiling](/create/scaling#max-instances) is capping traffic. Raise it, or raise concurrency if instances are underused. - **Bandwidth out spiking** - remember [container bandwidth is metered](/billing/billing#what-counts-as-usage); static assets belong on the CDN. ## History The last 20 deploys of each environment are kept with commit, duration, status, and build steps - see [Deploy history](/platform/deployments#deploy-history). The activity tab records what changed outside of deploys too: settings edits and environment-variable changes, with who and when - and [never the values](/platform/environment-variables#history-without-leaks). ## Ask instead of digging The [MCP server](/deploy-with-ai/mcp-server) reads the same logs: ask Claude "why did the last deployment fail?" and it fetches the deploy record and the logs, and summarizes what it finds. ## Related - [Builds](/platform/deployments/builds): Reading a failed build's extracted error lines. - [Scaling](/create/scaling): The settings the charts tell you to change. - [Limits](/reference/limits): Retention windows for logs, metrics, and history. --- # Deploy with AI You can go from no account to a running app on a paid plan without opening a dashboard. Install [Light Cloud AI](/deploy-with-ai/mcp-server), the LCAI MCP server (npm: [`@light-cloud/mcp-server`](https://www.npmjs.com/package/@light-cloud/mcp-server)) in Claude Code or GitHub Copilot and ask; script the same platform with the [Light Cloud CLI](/deploy-with-ai/cli) (npm: [`@light-cloud/cli`](https://www.npmjs.com/package/@light-cloud/cli)); or use the @lightcloud extension in VS Code. ## Three ways in - [Light Cloud AI (MCP server)](/deploy-with-ai/mcp-server): LCAI is the MCP server. Install @light-cloud/mcp-server in Claude Code, Claude Desktop, or GitHub Copilot in VS Code, sign in - or sign up - with a code, and ask it to create apps, deploy, read logs, pick a plan. - [Light Cloud CLI](/deploy-with-ai/cli): lc login, lc deploy, lc logs -f, lc billing. Every command takes --json, so it suits agents and CI as much as people. - [GitHub Copilot (VS Code)](/deploy-with-ai/vscode-copilot): Use @lightcloud in Copilot Chat with slash commands like /deploy and /status. ## Nothing left for the dashboard Two things used to need the console: creating an account and adding a card. Neither does now. - **Sign up from the terminal.** `connect` (MCP) or `lc login --device` sends you an email and shows a code. Approve on any device - a phone will do - and the account, workspace and free plan exist. No password is ever typed into a chat or a terminal. - **Pay from the terminal.** `add-payment-method` (MCP) or `lc billing card add` prints a Stripe-hosted link. Open it anywhere, save the card, and the tool picks it up; `choose-plan` / `lc billing plan use` does the rest. Card numbers never pass through the tools. - **Refusals say what to do next.** When a plan does not include a size, the free plan's included usage is used up, or a card is missing, the error names the tool or command that unblocks it, so an agent keeps going instead of stopping. All three talk to the same platform as the console - an app created from Claude shows up on the dashboard like any other, with the same [environments](/platform/preview-environments), [logs](/platform/observability), and settings. ## What is handled for you - [Framework detection](/platform/frameworks#how-detection-works) from the repository or the local folder. - Environment variables [read from your `.env` file](/platform/environment-variables#from-a-env-file-automatically). - A live URL and a link to the console on every deploy. - A small [project config file](/deploy-with-ai/mcp-server#follow-up-commands) so follow-up commands need no arguments. ## What it looks like > Deploy this project to Light Cloud. > Create a staging environment for storefront and deploy the develop branch to it. > Why did the last deployment fail? Check the logs. The assistant picks the right tools, chains them - look up the deployment, fetch the logs, summarize - and answers with links you can click. ## Good to know - **No repository required.** Both paths can [deploy a local folder](/create/upload) as an upload. - **Same permissions as you.** The tools act as your signed-in user; sign out any time from the conversation, or from Settings > Security > Connected devices in the console. - **Everything stays inspectable.** Deploys from AI land in the same [deploy history](/platform/deployments#deploy-history) with the same logs as any push. --- # Light Cloud AI: the MCP server Light Cloud AI (LCAI) is the MCP server that makes your cloud AI-ready: an assistant can sign you up, create apps, deploy, manage databases and environments, pick a plan and add a card - the whole path from nothing to a running app, from a conversation. It is published on npm as [`@light-cloud/mcp-server`](https://www.npmjs.com/package/@light-cloud/mcp-server) and works with Claude Code, Claude Desktop, and GitHub Copilot in VS Code - or any other client that speaks MCP over stdio. > [!TIP] > Prefer a walkthrough? The [Light Cloud AI](https://www.light-cloud.com/deploy-with-ai) page shows the whole loop - install, ask, approve, live URL - step by step for Claude Code and GitHub Copilot. ## Requirements - Node.js 18 or later (the install commands below use `npx`, which ships with it). - A Light Cloud account - or not. The `connect` tool creates one for an email that has none. ## Install ### Claude Code Add the server with one command: ```bash claude mcp add light-cloud -- npx @light-cloud/mcp-server ``` Verify it's connected: ```bash claude mcp list ``` By default, Claude Code asks before each tool call. To approve the Light Cloud tools once, open `/permissions` and add `mcp__light-cloud` to the allow list - that covers every tool the server provides. To remove the server later: `claude mcp remove light-cloud`. ### Claude Desktop The quickest route is the desktop extension: download the latest `light-cloud-.mcpb` from the [releases page](https://github.com/light-cloud-com/mcp-server/releases/latest), double-click it, and Claude Desktop installs the server with everything it needs. No Node.js, no config file. Updates are the same file again; the extension keeps your sign-in. Or add the server by hand to `claude_desktop_config.json`: - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "light-cloud": { "command": "npx", "args": ["@light-cloud/mcp-server"] } } } ``` Restart Claude Desktop after saving. The Light Cloud tools appear under the tools icon in the chat input. ### GitHub Copilot (VS Code) VS Code runs MCP servers for Copilot Chat's agent mode. Add the server to your project by creating `.vscode/mcp.json`: ```json { "servers": { "light-cloud": { "type": "stdio", "command": "npx", "args": ["@light-cloud/mcp-server"] } } } ``` Or from a terminal: ```bash code --add-mcp '{"name":"light-cloud","command":"npx","args":["@light-cloud/mcp-server"]}' ``` Save the file and pick **Start** above the server entry (VS Code may ask you to trust it first). For a user-wide install that applies to every workspace, run **MCP: Add Server** from the Command Palette and choose the *Global* target instead. Then open Copilot Chat, switch to **Agent** mode, and check that the Light Cloud tools are ticked under **Configure Tools**. Copilot asks before running each tool; choose *Always allow* to skip the prompt for that tool next time. To remove the server later, delete the entry from `mcp.json`. ## Sign in (or sign up) Ask, with your email: > Connect me to Light Cloud as julia@example.com. The `connect` tool answers with an 8-character code and the address `console.light-cloud.com/device`. Open it on any device - a phone is fine - sign in, type the code, approve. The assistant calls `connect-status` until the approval lands. No browser is needed on the machine running Claude, which is what makes this work over SSH, in containers and in Codespaces. If the email has no account yet, the email we send carries a link: open it, type the code, and approving creates the account and a workspace on the free plan. No password is set until you choose one in the console; no password ever passes through the chat. The older `login` tool - a browser tab on this machine with a loopback callback - still works. Credentials are stored locally in `~/.lightcloud/credentials.json`. The tools act as your signed-in user - they can do what you can do, nothing more. Check or end the session from the conversation, or sign a device out from **Settings > Security > Connected devices** in the console: > Am I logged in to Light Cloud? > Log me out of Light Cloud. ## Deploy your first app ### From a local project Open Claude Code in your project directory (or Copilot Chat with the project open in VS Code, or Claude Desktop with the folder connected) and ask: > Deploy this project to Light Cloud. The assistant asks at most two things before it touches anything, in one message: which workspace, if you belong to several, and whether the site should be public or behind a password. Then it packages the folder, uploads it, and deploys - see [Deploy from an upload](/create/upload) for what upload apps are. No Git remote required. It waits for the build and replies with the live URL on `light-cloud.io` (and the password, if you chose one), plus a link to the app in the console. Nothing else: no build settings to confirm, no plan to pick - the free plan is the default and a paid one is only raised if a tool refuses. What it does not ask is what kind of project this is. The uploaded folder is read by the same detector the console uses for a connected repository, so an upload deploys exactly the way the same code would from GitHub: Next.js becomes a server app (or a static site when `output: "export"` is set), Vite and Create React App become static sites with the right output directory, a `requirements.txt` with FastAPI becomes a Python container on port 8000, a `Dockerfile` is used as-is with its `EXPOSE` port. The catalogue covers Next.js, Nuxt, SvelteKit, Remix, Astro, React, Vue, Angular, Svelte, SolidJS, Qwik, Gatsby, Docusaurus, Eleventy, Hugo, Jekyll, plain HTML, Express, Fastify, Hono, NestJS, AdonisJS, Django, Flask, FastAPI, Rails, Sinatra, Laravel, Symfony, WordPress, Spring Boot, Quarkus, Gin, Echo, Fiber, ASP.NET, Blazor, Wasp, and anything with a Dockerfile. If the build fails, the reply says so and points at the environment logs; "why did it fail?" reads them and explains. A failure never takes down a version that was already live. ### Behind a password > Deploy this project to Light Cloud behind a password. The site asks visitors for the password from its first request - the gate sits at the edge, in front of the app, so nothing is served without it. The assistant hands back the URL and the password together. Later: > Change the password on storefront to "spring-preview". > Make storefront public. Use it for client previews, staging, or anything you want to share by link without publishing. ### From a GitHub repository > Create a Light Cloud app from my acme/storefront repo and deploy the main branch. If your GitHub account isn't connected yet, Claude sends you the installation link for the Light Cloud GitHub App first, then creates the app and deploys the branch you asked for. From then on, [every push deploys](/platform/deployments) - the MCP server is how you set it up, not something in the path of your pushes. ### Follow-up commands On the first deploy, a small `.lightcloud` config file is written to the project directory. It records which application the folder belongs to, so later requests need no arguments: > Redeploy. > What's the status of this app? > Show me the latest logs. ## Everyday examples > List my applications. > Create a staging environment for storefront and deploy the develop branch to it. > Why did the last deployment of storefront fail? Check the logs. > Delete the preview environment for storefront. Claude combines tools on its own - asking "why did the deploy fail?" makes it look up the deployment, fetch the [environment logs](/platform/observability#runtime-logs), and summarize what it finds. ## Plan and payment Everything you would otherwise do under Billing in the console: > What plan is my workspace on, and how much of the included usage is used? > Show me the plans. Put the workspace on Pro. > Add a card. `get-billing` reads the plan, card and included usage; `list-plans` and `choose-plan` change plan (Free needs no card; its plan id is `hobby`). `add-payment-method` prints a Stripe-hosted link - open it anywhere, save the card, and `payment-method-status` picks it up; no card number is ever typed into the chat. When a paid plan is chosen without a card, the tool says so and names `add-payment-method` as the next step. ## From nothing to running The server ships a prompt, `deploy-from-scratch`, that walks an assistant through the whole path in order: sign in or sign up, ask which workspace and whether the site should be public or password-protected, detect the framework, create the app from the repository or an upload, add a database and environment variables if the framework needs them, deploy, wait for the build, hand back the URL (and the password, if you chose one). In Claude Code, `/light-cloud:deploy-from-scratch` starts it. Under the hood, `upload-and-deploy` and `create-application` take a `password` and answer with the URL, the console link and the environment id; `wait-for-deployment` blocks until the build is live and returns the URL, so the assistant is not polling status tables; `set-password-protection` changes or removes the gate. Every refusal a tool can return (a plan that lacks a size, a free plan whose included usage is used up, a missing card, a suspended workspace) carries a `Next step:` line, so the assistant calls that tool and retries rather than stopping. Tool results carry only what you can act on - the app's `light-cloud.io` URL, the console link, ids - never the infrastructure underneath. ## What stays in the console Three things are deliberately not tools: changing the account password, two-factor settings, and the switch described next. Everything else the console can do, an assistant can do. ## Turning agents off Settings → Security → **Agents & CLI** in the console decides what the CLI, the MCP server and the VS Code extension may do on your account. One master switch refuses every agent request; below it, groups you can keep console-only: creating and deploying, deleting, changing settings, databases, plan and payment, workspace and members, API keys. Reading is always allowed. **Sign out all agents** ends every agent session at once. It is enforced on the backend from the session itself: a session issued to the CLI or an MCP server carries that in its token, fixed when it was created, so an agent cannot pass as the console by changing a header. The card can only be changed from a console session. When an assistant hits the switch it gets a refusal (`AGENT_ACCESS_DISABLED` or `AGENT_ACTION_BLOCKED`) that says where to change it, and it is told not to retry or look for another route. API keys are separate: they are bound to a workspace, not a person, and are revoked under the workspace's API keys. ## Tool reference You don't call tools directly - ask in plain language and the assistant picks the right ones. Each row gives the tool, what it does, and a prompt to copy that reaches it. ### Account | Tool | What it does | Ask | |---|---|---| | `connect` | Sign in - or sign up - with an email and a short code you approve on any device | "Connect me to Light Cloud as you@example.com." | | `connect-status` | Wait for that approval | *(called for you)* | | `login` | Sign in with a browser on this machine | "Log me in to Light Cloud with the browser." | | `whoami` | Who is signed in | "Am I logged in to Light Cloud?" | | `get-profile` | Your account and workspaces | "Which Light Cloud workspaces am I in?" | | `logout` | End the session on this machine | "Log me out of Light Cloud." | ### Deploy | Tool | What it does | Ask | |---|---|---| | `upload-and-deploy` | Package this folder, upload it, create the app (or redeploy the linked one), optionally behind a password | "Deploy this project to Light Cloud." / "Deploy this behind a password." | | `create-application` | Create an app from a GitHub, GitLab or Bitbucket repository | "Create a Light Cloud app from acme/storefront and deploy main." | | `create-stack` | Create and deploy an app from a stack template (for example Open SaaS) | "Set up an Open SaaS app called billing-demo." | | `list-repo-directories` | Folders in a repository branch, to pick a monorepo root | "Which folders are in acme/mono on main?" | | `wait-for-deployment` | Block until the build finishes and return the live URL | "Wait for the deploy and give me the URL." | | `deploy-application` | Rebuild the production environment | "Redeploy storefront." | | `deploy-environment` | Deploy one environment | "Deploy the staging environment of storefront." | | `rollback-deployment` | Put an earlier deployment back live, no rebuild | "Roll storefront back to the previous deployment." | | `set-password-protection` | Gate a site behind a password, rotate it, or open it | "Put a password on storefront." / "Make storefront public." | | `detect-local-framework` | What this folder is, read locally (the upload is re-read by the backend) | "What kind of project is this?" | | `detect-framework` | What a repository is, read from the branch | "What would Light Cloud detect in acme/storefront?" | | `detect-local-git` | Remote, branch and dirty state of this folder | "Is this folder a git repo, and which branch?" | | `package-source`, `request-upload-url`, `complete-upload`, `create-application-from-upload` | The steps behind `upload-and-deploy`, for clients that want them separately | *(called for you)* | | `read-project-config`, `write-project-config` | The `.lightcloud` link in this folder | "Which app is this folder linked to?" / "Unlink this folder." | ### Apps and environments | Tool | What it does | Ask | |---|---|---| | `list-applications`, `get-formatted-list` | Everything in a workspace | "List my Light Cloud apps." | | `get-application`, `get-application-status`, `get-formatted-status` | One app with its environments, URLs and last deploy | "What's the status of storefront?" | | `list-environments`, `get-environment` | Environments of an app | "Show the environments of storefront." | | `create-environment` | New environment from a branch | "Create a staging environment for storefront from the develop branch." | | `delete-environment` | Remove an environment | "Delete the preview environment of storefront." | | `set-scaling` | Instance floor and ceiling; a floor of 1 keeps it always on | "Keep storefront always on." / "Let staging scale to zero." | | `update-application` | Build command, output directory, monorepo root, framework, runtime, port, default size, auto-deploy branches, GitHub checks | "Change storefront's build command to pnpm build." | | `update-environment` | One environment's name, build, port, memory, cpu, instance limits, auto-deploy | "Give staging 1 GB of memory." / "Stop staging from deploying on push." | | `rename-application`, `move-application` | Rename; move into a folder | "Rename storefront to shop." / "Move shop into the Clients folder." | | `list-folders`, `create-folder`, `delete-folder` | Folders that group apps and databases | "Create a folder called Clients." | | `get-environment-metrics` | Requests, latency, errors, instances, cpu, memory | "How is storefront doing in the last 24 hours?" | | `get-environment-activity` | Who changed what on an environment | "Who changed staging last week?" | | `get-environment-runtime` | What is running now: live deployment, instances, size, region | "What is production running right now?" | | `delete-application` | Remove an app and every environment | "Delete the storefront app." | ### Logs and deployments | Tool | What it does | Ask | |---|---|---| | `get-environment-logs` | Runtime logs, newest first | "Show me the last 100 log lines of storefront." / "Any errors in staging in the last hour?" | | `list-deployments`, `get-deployment` | History and one deployment | "Show the last five deployments of storefront." | | `get-build-logs` | The build log of one deployment | "Why did the last deployment of storefront fail?" | ### Variables, databases, domains | Tool | What it does | Ask | |---|---|---| | `set-environment-variables` | Merge variables into an environment (empty string clears one) | "Set STRIPE_KEY on storefront production to sk_… and redeploy." | | `get-environment-variables` | Variable names, values masked | "Which variables are set on storefront?" | | `create-database` | A managed database - PostgreSQL on the Shared tier by default (Lite and up) | "Add a Postgres database for storefront." | | `list-databases`, `get-database` | Databases and their state | "Is the database ready?" | | `get-database-connection-string` | The secret connection string - it goes straight into a variable | "Set DATABASE_URL on production from the new database." | | `add-custom-domain`, `get-custom-domain-status` | Attach a domain, get the DNS records, check verification | "Point shop.example.com at storefront." / "Has the domain verified?" | | `retry-custom-domain`, `remove-custom-domain` | Retry the certificate after fixing DNS; detach the domain | "Retry the certificate for shop.example.com." / "Remove the custom domain from storefront." | | `update-database` | Name, tier, region, storage, high availability | "Grow shop-db to 50 GB." | | `get-database-schema` | Schemas, tables, columns, row counts | "What tables are in shop-db?" | | `query-database` | Run SQL, read-only unless asked otherwise | "How many orders are in shop-db since Monday?" | | `get-database-metrics` | Connections, cpu, memory, storage, query load | "Is shop-db under load?" | | `dump-database`, `import-database` | Download a compressed dump to a local file; load a dump from a file | "Back up shop-db to ./backups." / "Import backup.sql.gz into shop-db." | | `rotate-database-password` | New admin password; then refresh DATABASE_URL | "Rotate the password on shop-db and update production." | | `delete-database` | Delete a database and its data | "Delete the old-demo database." | ### Plan and card | Tool | What it does | Ask | |---|---|---| | `get-billing` | Plan, card on file, included usage this cycle | "What plan is my workspace on, and how much of the included usage is used?" | | `list-plans` | Plans and what they include | "Show me the plans." | | `choose-plan` | Switch plan (paid plans need a card) | "Put the workspace on Pro." | | `add-payment-method`, `payment-method-status` | Save a card through a Stripe-hosted link, wait for it | "Add a card." | | `remove-payment-method` | Remove the card on file (free plan, nothing unpaid) | "Remove my card." | | `get-usage`, `get-usage-history` | Usage this cycle, per resource; previous cycles | "What is using my included usage this month?" | | `list-invoices`, `get-invoice`, `retry-invoice`, `get-outstanding-invoices` | Invoices, one invoice, charge a failed one again, what is unpaid | "Show my invoices." / "Retry the failed invoice." | | `get-usage-alerts`, `set-usage-alerts` | One extra usage-alert email at a dollar amount (the 80% and 100% emails always send) | "Email me when this workspace passes $50 this month." | | `get-billing-details`, `set-billing-details` | Billing address and tax ids on invoices | "Put our VAT number on the invoices." | ### Git providers | Tool | What it does | Ask | |---|---|---| | `get-github-installation-status`, `get-github-install-url`, `list-github-installations` | Whether the Light Cloud GitHub App can read a repository; the link to install it | "Can Light Cloud see acme/storefront?" | | `list-repositories`, `list-branches`, `check-repo-access` | Repositories and branches the app can reach | "Which of my repos can Light Cloud deploy?" | | `connect-git-provider` | One link that signs in and authorises GitLab or Bitbucket for the workspace | "Connect my GitLab account." | | `list-provider-repositories` | Repositories reachable through a connected GitLab or Bitbucket account | "Which GitLab projects can Light Cloud deploy?" | ### Workspace, members, account | Tool | What it does | Ask | |---|---|---| | `create-workspace` | A new workspace. Each person gets one free workspace; a second or later one cannot create apps or deploy until it is on a paid plan | "Create a workspace called Clients." | | `list-members`, `invite-member`, `remove-member`, `set-member-role`, `list-roles` | Who is in the workspace and what they may do | "Invite sam@example.com as an admin." | | `list-api-keys`, `create-api-key`, `revoke-api-key` | API keys for CI and other machines. Paid plans only; the secret is shown once | "Create an API key for CI." | | `update-profile` | Your name and time zone | "Set my time zone to Europe/Warsaw." | | `list-connected-devices`, `sign-out-device` | Every signed-in session; sign one out | "Which devices are signed in?" / "Sign out the old laptop." | | `get-agent-access` | What this account lets agents do (read-only; changed in the console) | "What am I allowed to do from here?" | | `list-notifications`, `mark-notifications-read` | Account notifications | "Anything new?" | | `contact-support` | A message to Light Cloud support | "Tell support the build cache seems stale." | ### Platform | Tool | What it does | Ask | |---|---|---| | `ping`, `get-platform-config`, `get-cloudrun-config` | Reachability, supported frameworks and runtimes, container sizes | "Which frameworks does Light Cloud support?" | Prompt: `deploy-from-scratch` - in Claude Code, `/light-cloud:deploy-from-scratch`. > [!NOTE] > Destructive tools exist (`delete-application`, `delete-environment`) - Claude confirms before using them, and Claude Code's permission prompts are a second gate. Keep them out of any auto-approve list if that worries you. Nothing in the server can read a card number or a password: those steps always open a page you complete yourself. ## Use a different API endpoint The server talks to `https://api.light-cloud.com` by default. To point it at a staging or self-hosted API, set `LIGHT_CLOUD_API_URL`: ```bash claude mcp add light-cloud --env LIGHT_CLOUD_API_URL=https://api.staging-light-cloud.com -- npx @light-cloud/mcp-server ``` Or in the Claude Desktop config: ```json { "mcpServers": { "light-cloud": { "command": "npx", "args": ["@light-cloud/mcp-server"], "env": { "LIGHT_CLOUD_API_URL": "https://api.staging-light-cloud.com" } } } } ``` ## Troubleshooting - **Tools don't show up.** In Claude Code, run `claude mcp list` and check the server status. In Claude Desktop, make sure the config file is valid JSON and restart the app. In VS Code, open `mcp.json`, check the server shows *Running*, and make sure Copilot Chat is in Agent mode with the tools ticked under Configure Tools. - **Sign-in loops or expired session.** Ask Claude to log out and connect again. As a hard reset, delete `~/.lightcloud/credentials.json` and sign in fresh. - **"Device sign-in is not enabled".** The environment the server points at has the code flow switched off; use the `login` tool (browser on this machine) instead. - **Wrong app targeted.** The folder's `.lightcloud` file decides - ask Claude what this project's config says, or delete the file and deploy fresh. - **"Light Cloud storage refused the upload".** A fault on our side, not your plan or permissions - the message says so. Deploy from a repository in the meantime and tell support; it is not something to fix in your project. - **The build failed.** Ask "why did the build fail?" - the assistant reads the environment logs. The usual causes are a build command that needs a variable you have not set yet, or a Dockerfile that expects a file the folder does not have. A previously live version keeps serving. - **The site shows "Password required".** That is the gate you asked for. The assistant printed the password with the URL; "make it public" removes the gate. - **The site returns 403 right after the deploy.** The edge can reach the app a few seconds before it is fully wired up. Wait a moment and reload; if it persists past a minute, ask for the status. ## Related - [Deploy with AI](/deploy-with-ai/overview): Both AI paths at a glance. - [Light Cloud CLI](/deploy-with-ai/cli): The same platform as commands: lc login --device, lc deploy, lc billing. - [GitHub Copilot (VS Code extension)](/deploy-with-ai/vscode-copilot): The @lightcloud slash-command extension, an alternative to MCP inside VS Code. - [Deploy from an upload](/create/upload): What a no-repository app is and how it ships. --- # Light Cloud CLI `lc` is Light Cloud from the terminal: sign in, create apps, deploy, tail logs, manage environments, databases, domains and billing. It is published on npm as [`@light-cloud/cli`](https://www.npmjs.com/package/@light-cloud/cli) and talks to the same platform as the console and [Light Cloud AI](/deploy-with-ai/mcp-server) - an app created with `lc` shows up on the dashboard like any other. Every command takes `--json`, so the CLI is as usable by an agent or a CI job as by a person. ## Requirements - Node.js 18 or later. - A Light Cloud account - or not: `lc login --device` creates one for an email that has none. ## Install ```bash npm install -g @light-cloud/cli ``` Or run it without installing: ```bash npx @light-cloud/cli login ``` Both `lc` and `light-cloud` are installed as commands. ## Sign in Three ways, for three situations: ```bash lc login # opens your browser (Google, GitHub, or email) lc login --device # a code to type on any device, no browser needed here lc login --api-key lc_… # CI and machines, with a key from Organisation settings > API keys ``` ### With a code (`--device`) On a machine without a browser - an SSH box, a container, a Codespace - `lc login --device` asks for your email and prints an 8-character code: ``` │ Open https://console.light-cloud.com/device on any device, sign in, and enter the code: │ │ WXYZ-2345 │ │ The code expires in 15 minutes. ◐ Waiting for the code to be approved… ``` Open the link on anything with a browser (a phone works), sign in, type the code, approve. The terminal finishes on its own. If the email has no account yet, the email we send carries a link that creates one when you approve - no password, no form. On Linux with no `DISPLAY`, `lc login` picks this flow automatically. Devices signed in this way are listed under **Settings > Security > Connected devices** in the console, where they can be signed out. Credentials are stored in `~/.lightcloud/credentials.json`. `lc whoami` shows the account and its workspaces; `lc logout` forgets the session. ## Deploy your first app From a project folder: ```bash lc init # link this folder to an app (existing or new) lc deploy # deploy and follow the build lc logs -f # tail runtime logs ``` `lc init` detects the framework, asks the few questions it cannot answer from the folder, and writes a `.lightcloud` file so later commands need no arguments. No Git remote is required - a plain folder deploys as an [upload](/create/upload). With a connected GitHub repository, `lc create --repo https://github.com/owner/name` sets up an app that [deploys on every push](/platform/deployments). Add `--password ` to either form and the site asks visitors for it from the first deploy. Detection is the console's, not the CLI's. The folder is read locally first so `lc init` can show what it found, but once the archive is uploaded the backend inspects it with the same detector the console uses for a connected repository, and the app is created from that reading - so an upload and a repository deploy the same code the same way. An explicit flag (`--framework`, `--type`, `--build`, `--output`, `--port`) still wins over both. When the backend's reading differs from the local one, `lc` says so: ``` ℹ Light Cloud read the source as nextjs (container). ``` When the build finishes, `lc deploy` prints `Live at https://--.light-cloud.io`; with `--json` the same URL is in the `url` field. ## Command reference Every command below has a copy button. `[app]` defaults to the app linked in this folder, `--env` to `production`, and the workspace to your default one (`--org ` overrides it for one command). Add `--json` to any command for machine-readable output, `-y` to skip confirmations, and `--help` for the full flag list. ### Account and workspaces ```cmds lc login :: Sign in with a browser on this machine. lc login --device --email you@example.com :: Sign in with a short code you approve on any device. Creates the account if the email has none. lc login --api-key :: Sign in with an API key, for CI and other machines. LIGHT_CLOUD_API_KEY in the environment does the same. lc whoami :: Who is signed in and which workspaces they belong to. lc logout :: Sign out and forget the stored credentials. lc orgs :: List the workspaces you belong to. lc org use acme :: Set the default workspace for later commands. No name: pick from a list. lc org current :: Which workspace commands will use here. ``` ### Create and link an app ```cmds lc init :: Link this folder to an app - pick an existing one or create one - and write a .lightcloud file. lc init --app storefront --env production :: Link without asking. lc create --upload :: Create an app from this folder, deploy it and follow the build to the URL. lc create --upload --name storefront --password spring-preview :: Same, named, and gated behind a visitor password from the first deploy. lc create --repo https://github.com/acme/storefront --branch main :: Create an app from a connected GitHub repository; every push to the branch deploys. lc create --repo https://github.com/acme/mono --root apps/web :: Monorepo: build from a folder inside the repository. lc create --upload --framework nextjs --type container --port 3000 :: Override what detection found. See lc frameworks for ids. lc create --upload --no-deploy :: Create without deploying. lc frameworks :: Framework ids the platform understands, for --framework. lc unlink :: Remove the .lightcloud link file from this folder. ``` ### Deploy and inspect ```cmds lc deploy :: Deploy the linked app and follow the build to the live URL. lc deploy storefront --env staging :: Deploy a named app's environment. lc deploy --upload :: Send this folder again (upload apps redeploy from the last archive otherwise). lc deploy --no-watch :: Queue the deployment and return at once. lc apps :: Everything deployed in the workspace. lc apps store :: Filter apps by name. lc status storefront :: One app with its environments, URLs and latest deployment. lc open :: Open the live site in your browser. lc open --console :: Open the app in the Light Cloud console instead. lc rename storefront shop :: Rename an app. lc app update storefront --build "pnpm build" --output dist :: Change build settings; only the flags given change. Also --root, --framework, --runtime, --port, --memory, --cpu, --min, --max, --auto-deploy-branches, --github-checks, --github-pr-comments. lc app move storefront --folder :: Move an app into a folder (see lc folders); omit --folder for the root. lc app repo-dirs acme/mono --branch main :: Folders in a repository branch, for --root on monorepos. lc delete storefront :: Delete an app and every environment in it. ``` ### Folders and stacks ```cmds lc folders :: Folders (projects) that group apps and databases. lc folder create Clients :: Create a folder; --parent nests it. lc folder delete :: Delete an empty folder. lc stack create opensaas --name billing-demo :: Create and deploy an app from a stack template (needs a connected GitHub account for the repository it creates). ``` ### Logs and deployments ```cmds lc logs -f :: Follow production logs of the linked app. lc logs storefront --env staging --since 2h :: Logs of one environment, how far back to look. lc logs --min-severity WARNING --search "timeout" :: Only warnings and above that contain a string. lc logs -n 500 --no-timestamps :: More history, without timestamps. lc deployments :: The last 10 deployments of the linked app. lc deployments storefront --env staging -n 20 :: History of one environment, up to 20. lc deployment :: One deployment with its build log. lc rollback :: Go back to the previous deployment - no rebuild, the old image is switched in. lc rollback --env staging :: Roll one environment back to a specific deployment. ``` ### Environments ```cmds lc envs storefront :: Environments of an app. lc env get staging --app storefront :: One environment in detail: branch, URL, domain, size, variables count. lc env create staging --branch develop --deploy :: New environment from a branch, deployed right away. lc env create preview --branch feature/x --no-auto-deploy --min 0 --max 1 :: A preview that does not redeploy on push and scales to zero. lc env scale --min 1 :: Keep one instance always on (Starter and up). lc env scale staging --app storefront --max 3 :: Set the instance ceiling. lc env password --set spring-preview :: Visitors must enter this password; takes effect on the next request. lc env password --off :: Make the site public again. lc env update staging --memory 1Gi --auto-deploy off :: Change settings; only the flags given change. Also --name, --build, --output, --port, --cpu, --min, --max. lc env metrics --range 24h :: Requests, latency, errors, instances, cpu and memory. lc env activity :: Who changed what on the environment, newest first. lc env runtime :: What is running now: live deployment, instances, size, region. lc env delete preview --app storefront :: Delete an environment. ``` ### Environment variables ```cmds lc env vars :: Variable names, values masked. lc env vars list --reveal :: Values in clear. lc env vars set DATABASE_URL=postgres://… STRIPE_KEY=sk_… --redeploy :: Set one or more variables and redeploy so they take effect. lc env vars unset OLD_KEY --redeploy :: Remove variables. lc env vars import .env.production :: Merge a .env file over the existing variables. lc env vars import .env.production --replace :: Replace every variable with the file's contents. lc env vars export > .env.backup :: Print the variables in dotenv format. ``` ### Custom domains ```cmds lc domains add shop.example.com :: Attach a domain; prints the DNS records to create. lc domains :: The domain and its DNS and certificate status. lc domains check :: Re-check DNS after creating the records. lc domains retry :: Retry certificate issuance after fixing DNS. lc domains remove :: Detach the domain. ``` ### Databases ```cmds lc dbs :: Databases in the workspace. lc db tiers :: Engines, machine tiers and regions you can create. lc db create shop-db :: A PostgreSQL database on the Shared tier. Databases start on Lite, which allows one. lc db create shop-db --engine mysql :: MySQL instead. lc db create shop-db --tier --region europe-west1 --storage 20 --ha :: A dedicated tier with storage and high availability. lc db get shop-db :: Status, engine, tier, host. lc db url shop-db :: The connection string. Treat it as a secret. lc db url shop-db --details :: Host, port, database, user and password separately. lc env vars set DATABASE_URL="$(lc db url shop-db)" --redeploy :: Put the connection string straight into the app. lc db rotate-password shop-db :: New admin password; apps using the old one must be updated. lc db dump shop-db -o backup.sql.gz :: Download a compressed SQL dump. lc db import backup.sql.gz --db shop-db :: Load a dump into a database; existing data is kept. lc db schema shop-db :: Schemas, tables, columns and row counts. lc db query "select count(*) from orders" --db shop-db :: Run SQL, read-only unless --write. lc db metrics shop-db --range 1h :: Connections, cpu, memory, storage and query load. lc db update shop-db --storage 50 --ha :: Change name, tier, region, storage or high availability. lc db delete shop-db :: Delete the database and all its data. ``` ### Billing ```cmds lc billing :: Plan, card on file and how much of this cycle's included usage is used. lc billing plans :: Plans this workspace can be on. Free's id is hobby. lc billing plan use pro :: Switch plan. Upgrades confirm and charge the card; downgrades take effect at the next cycle. lc billing card :: The card on file. lc billing card add :: Save a card through a Stripe-hosted page; waits until it is saved. No card number passes through the CLI. lc billing card add --plan pro --no-browser :: Print the link instead of opening it, and switch plan once the card lands. lc billing card remove :: Remove the card on file. lc billing usage :: Usage this cycle, per resource, against the included amount. lc billing history :: Usage over previous cycles. lc billing invoices :: Invoices, newest first; --status paid|open|failed. lc billing invoice :: One invoice with its lines; --retry charges the card again for a failed one. lc billing outstanding :: Unpaid invoices across the workspaces you own. lc billing alerts --at 50 :: One extra email when usage passes $50 this cycle (the 80% and 100% emails always send); --clear removes it; no flags shows it. lc billing details --set company_name="Acme sp. z o.o." vat_number=PL123 country=PL :: Billing address and tax ids on invoices; no flags shows them. ``` ### Workspace and members ```cmds lc org create Clients :: Create a new workspace. Each person gets one free workspace; a second or later one cannot create apps or deploy until it is on a paid plan. lc members :: Members of the workspace with their roles. lc member invite sam@example.com --role admin :: Invite by email; they join once they sign in. lc member role user :: Change a member's role. lc member remove :: Remove a member. lc roles :: Roles available in the workspace. ``` ### API keys (paid plans) ```cmds lc keys :: API keys of the workspace. lc key create ci --role user :: Create a key for CI and other machines; the secret is shown once. --expires for an expiry. lc key revoke :: Revoke a key; anything using it stops at once. ``` ### Git providers ```cmds lc git connect gitlab :: One link that signs in and authorises GitLab for this workspace. Also bitbucket, or github for the App install link. lc git repos gitlab :: Repositories reachable through the connected provider. lc git github-status :: GitHub App installations linked to this workspace. ``` ### Account ```cmds lc profile :: Your name and time zone; --first-name, --last-name, --timezone change them. lc devices :: Every signed-in session: browsers, the CLI, MCP servers, VS Code. lc device sign-out :: Sign one session out. lc agent-access :: What this account lets agents do - the switch under Settings > Security > Agents & CLI, read-only here. lc notifications --unread :: Account notifications; --read [id] marks one or all as read. lc support "Build cache seems stale" -m "Details here" :: Send a message to Light Cloud support; --feature files it as a feature request. ``` ### CLI settings ```cmds lc config :: Effective settings and where each comes from. lc config set api-url https://api.staging-light-cloud.com :: Point the CLI at another API. lc config unset api-url :: Back to the default. ``` ## What stays in the console Changing the account password, two-factor settings, and the Agents & CLI switch are console-only. Everything else the console can do has a command above. ## Turning the CLI off Settings → Security → **Agents & CLI** in the console decides what the CLI, the MCP server and the VS Code extension may do on your account: a master switch, then groups you can keep console-only (deploying, deleting, settings, databases, plan and payment, workspace and members, API keys). Reading is always allowed. A refused command says so and points there: ``` ✖ Agents may not delete apps, environments and databases on this account. Allow it under Settings → Security → Agents & CLI in the console, or do this step there. ``` The switch is enforced from the session itself - `lc login` receives a session marked as the CLI's - so it holds regardless of flags or headers. `lc agent-access` shows the current setting. API keys are separate: bound to a workspace, revoked with `lc key revoke`. ## Billing from the terminal `lc billing` shows the plan, the card on file and how much of this cycle's included usage is used. `lc billing plans` lists what the workspace could be on; `lc billing plan use ` switches (upgrades confirm and charge the card, downgrades take effect at the next cycle). A card is added through Stripe's hosted page - `lc billing card add` prints a link, opens it if there is a browser, and waits until the card is saved. The link works on any device, and no card number ever passes through the CLI. Add `--plan pro` to switch plan as soon as the card is on file. When something needs a plan change or a card, the refusal says which command unblocks it: ``` ✖ A payment method is required for paid plans. Run lc billing card add, then this command again. ``` ## For agents and CI - `--json` on any command prints one JSON document on stdout and nothing else; errors go to stderr with a non-zero exit code (`2` usage, `3` auth, `4` not found). - `--yes` answers confirmations; `--org ` pins the workspace. - `LIGHT_CLOUD_API_KEY` in the environment signs in without a stored session - the usual setup for a pipeline running from a folder with a `.lightcloud` file. ```bash LIGHT_CLOUD_API_KEY=lc_… lc deploy --json --yes ``` Coding agents can use `lc` the same way they use `git`: the [MCP server](/deploy-with-ai/mcp-server) is the conversational route, the CLI the scripted one, and both reach every feature. ## Use a different API endpoint ```bash lc config set api-url https://api.staging-light-cloud.com ``` or `--api-url ` / `LIGHT_CLOUD_API_URL` for one command. ## Related - [Light Cloud AI (MCP server)](/deploy-with-ai/mcp-server): The same platform from a conversation in Claude Code, Claude Desktop or Copilot. - [Deploy with AI](/deploy-with-ai/overview): All the AI paths at a glance. - [API keys](/reference/api/authentication): Keys for pipelines and machines. --- # Deploy from GitHub Copilot The Light Cloud extension adds `@lightcloud` to GitHub Copilot Chat in VS Code: deploy the project you have open, check its status, and read the plan before anything ships - without leaving the editor. ## Requirements - VS Code with GitHub Copilot Chat enabled. - A Light Cloud account (created during `/login` if you don't have one). ## Install 1. Install the **Light Cloud** extension from the VS Code marketplace. 2. Open Copilot Chat and sign in: ``` @lightcloud /login ``` A browser window handles the sign-in, and the chat confirms when you're connected. ## Slash commands | Command | What it does | | --- | --- | | `/deploy` | Builds and deploys the current project | | `/redeploy` | Ships the latest changes to the same app | | `/status` | Shows the current deployment - state, URL, last deploy | | `/list` | Lists your applications | | `/plan` | Previews the configuration without deploying | | `/destroy` | Tears an application down | | `/login`, `/logout` | Manage your session | The extension writes a small `.lightcloud` project config on the first deploy, so follow-up commands run without arguments - the same file the [MCP server](/deploy-with-ai/mcp-server#follow-up-commands) uses. ## What a deploy does `/deploy` runs the same pipeline as the Create page: [detection](/platform/frameworks#how-detection-works) reads the project, variables in your local `.env` are [set on the app](/platform/environment-variables#from-a-env-file-automatically), the build runs, and you get the live URL and a console link in the chat. ``` @lightcloud /deploy Framework: React (Vite) Live at https://acme-app.light-cloud.io Console: https://console.light-cloud.com/apps/acme-app ``` No Git remote is required - a folder deploys as an [upload app](/create/upload). With a connected repository, pushes keep deploying [as usual](/platform/deployments); the extension is a convenience, not a dependency. > [!TIP] > Run `/plan` before the first `/deploy` on an unfamiliar project - it shows the detected framework, build settings, and target without shipping anything. ## Troubleshooting - **`@lightcloud` doesn't appear.** Check the extension is installed and enabled, then reload the window. Copilot Chat must be signed in and active. - **Session expired.** `/logout` then `/login` refreshes credentials. - **Wrong app targeted.** The folder's `.lightcloud` file decides which app `/redeploy` and `/status` talk to - delete it and `/deploy` fresh to re-link. ## Related - [Deploy with AI](/deploy-with-ai/overview): Both AI paths at a glance. - [Light Cloud AI (MCP server)](/deploy-with-ai/mcp-server): The same platform, driven from a conversation - also works inside Copilot Chat via mcp.json. - [Deploy from an upload](/create/upload): How no-repository apps work. --- # Pricing A plan is a fixed price that includes the same amount of usage every month. Everything you run counts against it at the same rates on every plan; on a paid plan, extra usage goes on the next invoice. What a plan lets you create - server apps, sizes, databases, always-on, team members - is a separate list of [plan limits](/reference/limits#plans). Free includes $1 of usage a month, with no card. A typical site uses about a cent of it. If you ever use it all, server apps pause and your sites stay up. Free never bills you. ## Plans | Plan | Price | Usage included | For | | --- | --- | --- | --- | | Free | $0 | $1 a month | Unlimited static sites, 3 server apps and a preview URL for every branch, each on a light-cloud.io address. No custom domain, no databases, a small "by Light Cloud" link on every page, 1 team member, no card. When the $1 is used up, server apps pause and sites stay up. | | Lite | $5 a month | $5 a month | Your own domain and no "by Light Cloud" link. 5 server apps of any kind and 1 shared database. | | Starter | $19 a month | $19 a month | 10 server apps, sizes up to Small, shared and dedicated databases, always-on instances, 3 team members. | | Pro | $49 a month | $49 a month | 30 server apps, sizes up to Medium, unlimited team members. | | Business | $149 a month | $149 a month | 100 server apps, sizes up to Large, unlimited team members. | Free is the plan formerly called Hobby; its id in the API, the CLI and the MCP server is still `hobby`. Every plan includes: - **Unlimited static sites**, with bandwidth included up to a [fair-use amount](#static-sites) per workspace. - **A preview URL for every branch**, with no limit on how many. See [Preview environments](/platform/preview-environments). - **Every kind of app**: static sites, server-rendered frontends, APIs and anything with a Dockerfile. More than 100 server apps, or an invoice instead of a card: [talk to us](https://www.light-cloud.com/contact?subject=sales). The full list of what each plan allows is under [Limits](/reference/limits#plans). How the included usage, the monthly cycle, extra usage and upgrading work is under [Billing](/billing/billing). ### Paying yearly Every paid plan can be paid yearly for the price of ten months - two months free: | Plan | Monthly | Yearly | | --- | --- | --- | | Lite | $5 | $50 | | Starter | $19 | $190 | | Pro | $49 | $490 | | Business | $149 | $1,490 | Included usage still resets every month: a yearly Starter plan includes $19 of usage each month. ## What counts as usage The rates below are the same on every plan. They are set in the console and shown next to every size and tier when you create a resource. ### Containers Counted per instance-hour, at one of two rates: - **Scale to zero** (the default, a minimum of zero instances): an instance counts only while it runs. An idle app [scales to zero](/billing/billing#scale-to-zero) and counts nothing, so a month of usage is the time your app actually spent serving. - **Always-on** (a minimum of one instance, Starter and up): the instance never scales to zero and counts every hour of the month at the always-on rate, shown here per month with its hourly rate. | Size | vCPU | Memory | Scale to zero, per hour while running | Always-on, per month | | --- | --- | --- | --- | --- | | Nano | 1 | 256 MB | $0.115 | Scale to zero only | | Micro | 1 | 512 MB | $0.12 | ~$52 ($0.071/h) | | Small | 1 | 1 GB | $0.125 | ~$55 ($0.075/h) | | Medium | 2 | 2 GB | $0.25 | ~$105 ($0.144/h) | | Large | 4 | 4 GB | $0.50 | ~$210 ($0.288/h) | Which sizes a plan may create is under [Limits](/reference/limits#plans); each size's maximum concurrency is under [Scaling](/create/scaling#concurrency). ### Databases Counted by the hour for the tier and storage you provision, from create to delete. Tier names are the ones the console shows. | Tier | vCPUs | RAM | Per hour | Per month | | --- | --- | --- | --- | --- | | Dev (shared) | Pooled | Pooled | $0.0055 | ~$4 | | Starter | Shared core | 0.6 GB | $0.012 | ~$9 | | Pro | Shared core | 1.7 GB | $0.036 | ~$26 | | Business | 1 | 4 GB | $0.077 | ~$56 | | Enterprise | 2 | 8 GB | $0.152 | ~$111 | | EnterprisePlus | 4 | 16 GB | $0.303 | ~$221 | Storage is $0.30 per GB per month, and high availability doubles the compute price. Databases start on Lite, which allows one Dev (shared) database; Starter and up can create as many as they need. See [Databases](/platform/databases). ### Static sites Static sites run no instances. They count their build minutes and their storage ($0.02 per GB per month) - about a cent a month for a typical site - which is why a [static export](/platform/frameworks/nextjs#static-export) costs next to nothing. Bandwidth is included. Each workspace has a fair-use amount every month, shared by all its sites: 20 GB on Free and 1 TB on every paid plan. Fair use is not a meter and is never charged. If a workspace goes past it, we email first, and a site is only paused after a warning. ### Everything else | Resource | Rate | | --- | --- | | Build minutes | $0.01 per minute | | Container bandwidth | $0.20 per GB | | Database storage | $0.30 per GB per month | | High availability | 2x the database compute price | Team members are part of the plan, not usage: see [Billing](/billing/billing#team-members). There are no per-project fees. ## Regions Apps and databases run in four tier-1 regions today: | Region | Location | | --- | --- | | `europe-west1` | Belgium (Europe) | | `us-central1` | Iowa (US Central) | | `us-east1` | South Carolina (US East) | | `us-west1` | Oregon (US West) | More tier-1 regions (Netherlands, Taiwan) are coming at the same rates. Premium tier-2 regions - London, Frankfurt, Tokyo, Singapore, and Sydney - will carry a 20% surcharge. ## Related - [Billing](/billing/billing): Included usage, extra usage, what pauses on Free, upgrading, yearly billing and usage alerts. - [Scaling](/create/scaling): The settings that decide how many instance-hours you use. - [Limits](/reference/limits): What each plan lets you create, in one table. --- # Billing The plans and rates are on [Pricing](/billing/pricing); this page is how they turn into an invoice. ## The model in short - A **plan** is a fixed price, paid at the start of each cycle - monthly, or yearly for two months free. - Every plan **includes usage** worth its price: $5 on Lite, $19 on Starter, $49 on Pro, $149 on Business. Free includes $1 of usage a month at no cost. - Everything you run is **metered at the same rates on every plan** and counts against the included usage. - **Extra usage** - anything above what is included - is added to your next invoice at the same rates. Nothing is charged mid-cycle, and nothing pauses on a paid plan unless you set a [usage limit](#usage-limit) that pauses. - **Free never bills you. It pauses.** When its $1 is used up, server apps and deploys pause until the next cycle or an upgrade; static sites keep serving. - Unused usage **does not roll over**, and it is not refunded. - **Plan limits** - how many server apps, which sizes, databases, always-on, team members - are separate from usage. See [Limits](/reference/limits#plans). ## What counts as usage Infrastructure is metered by the hour as it runs, with a minimum of one hour per resource. - **Containers** count per instance-hour: at the scale-to-zero rate while an instance runs, or at the always-on rate for every hour of an always-on app (see [Pricing](/billing/pricing#containers)). If your app auto-scales to three instances for an hour, that is three instance-hours. Removing a container stops its meter; the hours it ran still count. - **Databases** count for the tier and storage while provisioned - a database is "running" from create to delete. - **Build minutes** are metered per deployment, with a minimum of one minute. Failed builds meter their minutes too. - **Bandwidth** is metered on container traffic only. Static-site bandwidth is included, up to a [fair-use amount](/billing/pricing#static-sites) per workspace. - **Storage** counts for static-site files and database disks. The billing page shows one bar - usage this cycle against the included amount - with every resource underneath and what it has used so far, including resources you have removed this cycle. The rows add up to the bar. ## Scale to zero Container apps default to zero [minimum instances](/create/scaling#min-instances). Usage starts when the app scales up and stops when it scales back to zero, so an idle app counts nothing. This is what makes many [preview environments](/platform/preview-environments) cheap. Apps with a minimum of one instance count around the clock, at the always-on rate. What a month typically comes to: | What you run | Usage a month | | --- | --- | | A static site, deployed once | about $0.01 | | A preview URL for a static branch, built twice | about $0.02 | | A server app at 300 requests a day | about $0.10 | | A server app at 3,000 requests a day | about $0.64 | | A server app at 15,000 requests a day | about $3.04 | | A shared database | $4.00 | The server apps run on Micro at about 0.2 seconds of work per request, with two deploys of two build minutes each. So Free's $1 covers 50 sites, 5 previews and 3 quiet server apps (about $0.90), and a busy app fits in Lite's $5. Always-on has its own rate, counted every hour: an always-on Small (a minimum of one instance, Starter and up) is about $55 a month, so on Starter, which includes $19, it adds about $36 of extra usage to the next invoice. Keep always-on for the apps whose cold starts hurt. ## Extra usage When usage passes what a paid plan includes, the bar turns red and keeps counting. Everything keeps running. At the end of the cycle the difference is added to the next invoice as one line, at the same rates, next to the plan price for the month ahead. You get an email at 80% of the included usage and another when it is used up. If you keep going past the included amount every month, the next plan up is usually cheaper than the extra usage. ## The free plan Free includes $1 of usage a month, needs no card and never bills you. There is nobody to invoice on a free workspace, so when the $1 is used up: - **Server apps pause**: visitors see a paused page instead of the app. - **Deploys pause**: new deploys are refused. - **Static sites keep serving.** A static site is never paused for usage. Everything resumes when the next billing cycle starts, or within a few minutes of an [upgrade](#upgrading). There is no grace period and nothing to pay. Before it happens, the console shows a banner at 50% of the $1, and an email goes out at 80% and again when it is used up. Each person gets one free workspace. You can create more workspaces, but a second or later one cannot create apps or deploy until it is on a paid plan. Free workspaces that existed before this rule keep working. Free does not include databases or custom domains. To move a workspace with a database to Free, download a copy from the database's Backup tab and delete the database first. A database or custom domain that is already on a Free workspace keeps working for 14 days after we email the owner. After that the database stops: apps cannot connect to it, the data is kept, and you can still download a copy. The custom domain goes on hold: visitors see a holding page, and the site keeps serving at its light-cloud.io address. Moving to a paid plan restarts both at once. ## Upgrading Upgrade from Billing in the console, or from the prompt that appears when you reach for something Free does not include - a custom domain, a database, a fourth server app - or when server apps are paused. One Stripe Checkout page takes your card and the first payment together; the plan switches as soon as it is paid, and paused server apps resume within a few minutes. With a card already saved, upgrading is one click. Checkout also offers [yearly billing](/billing/pricing#paying-yearly) and an optional [usage limit](#usage-limit). From an assistant or a terminal, see [Light Cloud AI](/deploy-with-ai/mcp-server#plan-and-payment) and the [CLI](/deploy-with-ai/cli#billing-from-the-terminal). ## Cycle and invoices Your billing cycle is monthly and starts on the day you choose a plan - not on the calendar month. One invoice is issued at the start of each cycle: the plan price for the month ahead, plus any extra usage from the cycle just finished. It is charged to the card on file automatically. On yearly billing the plan price is paid once a year, for the price of ten months ([prices](/billing/pricing#paying-yearly)). The cycle and its included usage still reset every month: a yearly Starter plan includes $19 of usage each month, not $228 at once. - **Free to paid**: the first month (or year) is paid at checkout, and a new cycle starts that day. - **Paid to a bigger plan**: the difference for the rest of the current cycle is charged now, and the bigger included usage applies to the whole cycle immediately. - **Down to a smaller plan**: takes effect at the end of the current cycle. The plan you already paid for runs to the end of the cycle, and that cycle's extra usage is measured against its included usage. A paid plan needs a card on file. Invoices that cannot be paid follow a reminder schedule; unpaid invoices are the only path to a suspended workspace. ## Usage alerts The 80% and 100% emails always send. Under Billing you can also add one more alert at a dollar amount of your choosing - useful when you want to hear about it well before the included usage runs out. Alerts only email; they never pause anything. ## Usage limit A paid plan never pauses on its own: extra usage goes on the next invoice. If you would rather have a ceiling, set a usage limit at an amount of extra usage you choose. It either alerts you when extra usage reaches that amount, or pauses server apps there; static sites keep serving either way. The usage limit is offered at checkout and is off unless you choose it. ## Team members Each plan includes a number of team members: one on Free and Lite, three on Starter, unlimited on Pro and Business. Members are part of the plan, not usage, and there are no paid extra seats: a fourth member needs Pro. ## Keeping usage down - Leave [min instances at 0](/create/scaling#min-instances) everywhere cold starts don't hurt. - Serve frontends as [static builds](/platform/frameworks#frontend) - they run no instances at all, and their bandwidth is included. - Use [shared-tier databases](/platform/databases#the-shared-tier) for previews and development. - Let [stale previews clean up](/create/git-automation#remove-stale-environments) with their branches. ## Related - [Pricing](/billing/pricing): The plans and every rate, by the hour. - [Scaling](/create/scaling): Instance-hours are a scaling decision. - [Limits](/reference/limits): What each plan lets you create, in one table. --- # API Everything the console does, it does over a public HTTP API. The same API backs the [MCP server](/deploy-with-ai/mcp-server), so anything an AI agent can do to your account, a script can do too. Base URL: ``` https://api.light-cloud.com ``` Every path below is relative to it. All requests and responses are JSON; send `Content-Type: application/json` on anything with a body. > [!NOTE] > This API is stable enough to build on but is not versioned yet. Endpoints are added more often than they change, and breaking changes are announced on the [blog](https://blog.light-cloud.com) before they ship. ## Authenticating Create an API key in the console - **left sidebar → your workspace name → API keys → New key** - and send it as a bearer token: ``` Authorization: Bearer lc_... ``` ```bash curl -X POST https://api.light-cloud.com/api/applications \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"limit":100}' ``` The secret is shown once, at creation. Keys require a paid plan; the [MCP server](/deploy-with-ai/mcp-server) works on every account including free. See [Authentication](/reference/api/authentication) for access levels, expiry, revocation, and what a key deliberately cannot do. ## A worked example Deploy a repository and wait for it to go live, start to finish. Each tab is self-contained — no SDK, no shared setup. ```codetabs ### curl #!/usr/bin/env bash set -euo pipefail API=https://api.light-cloud.com AUTH=(-H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" -H "Content-Type: application/json") # 1. Let detection work out the build settings rather than guessing them curl -sS -X POST "$API/api/applications/detect-framework" "${AUTH[@]}" -d '{"owner":"acme","repo":"shop","branch":"main"}' # 2. Create it. The production environment comes with it, and the first # build starts immediately. app=$(curl -sS -X POST "$API/api/applications/create" "${AUTH[@]}" -d '{ "name": "shop", "githubRepoUrl": "https://github.com/acme/shop", "githubBranch": "main", "deploymentType": "container", "framework": "nextjs", "runtime": "nodejs", "buildCommand": "npm run build", "environmentVars": { "NODE_ENV": "production" } }') APP_ID=$(echo "$app" | jq -r .id) # 3. Poll until the build settles while :; do status=$(curl -sS -X POST "$API/api/applications/status" "${AUTH[@]}" -d "{"applicationId":"$APP_ID"}" | jq -r .status) case "$status" in healthy) break ;; failed|degraded) echo "build failed"; exit 1 ;; esac sleep 10 done curl -sS -X POST "$API/api/applications/get" "${AUTH[@]}" -d "{"applicationId":"$APP_ID"}" | jq -r .url # https://main-shop-acme.light-cloud.io ### Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; // 1. Let detection work out the build settings const detected = await api("/api/applications/detect-framework", { owner: "acme", repo: "shop", branch: "main", }); // 2. Create it — production environment and first build come with it const app = await api("/api/applications/create", { name: "shop", githubRepoUrl: "https://github.com/acme/shop", githubBranch: "main", deploymentType: detected.deploymentType, framework: detected.framework, runtime: detected.runtime, buildCommand: detected.buildCommand, environmentVars: { NODE_ENV: "production" }, }); // 3. Poll until the build settles const settled = ["healthy", "failed", "degraded"]; let status = app.status; while (!settled.includes(status)) { await new Promise((resolve) => setTimeout(resolve, 10_000)); ({ status } = await api("/api/applications/status", { applicationId: app.id, })); } if (status !== "healthy") throw new Error(`Build ${status}`); const live = await api("/api/applications/get", { applicationId: app.id }); console.log(live.url); ### Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() import time # 1. Let detection work out the build settings detected = api( "/api/applications/detect-framework", owner="acme", repo="shop", branch="main", ) # 2. Create it — production environment and first build come with it app = api( "/api/applications/create", name="shop", githubRepoUrl="https://github.com/acme/shop", githubBranch="main", deploymentType=detected["deploymentType"], framework=detected["framework"], runtime=detected.get("runtime"), buildCommand=detected.get("buildCommand"), environmentVars={"NODE_ENV": "production"}, ) # 3. Poll until the build settles SETTLED = {"healthy", "failed", "degraded"} status = app["status"] while status not in SETTLED: time.sleep(10) status = api("/api/applications/status", applicationId=app["id"])["status"] if status != "healthy": raise RuntimeError(f"Build {status}") print(api("/api/applications/get", applicationId=app["id"])["url"]) ### Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // Everything this does is visible: POST, bearer token, JSON in, JSON out. static String apiPost(String path, String json) throws Exception { var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com" + path)) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) throw new IllegalStateException(response.body()); return response.body(); } // 1. Detection String detected = apiPost("/api/applications/detect-framework", "{"owner":"acme","repo":"shop","branch":"main"}"); // 2. Create String created = apiPost("/api/applications/create", """ { "name": "shop", "githubRepoUrl": "https://github.com/acme/shop", "githubBranch": "main", "deploymentType": "container", "framework": "nextjs", "runtime": "nodejs", "buildCommand": "npm run build", "environmentVars": { "NODE_ENV": "production" } } """); String appId = jsonField(created, "id"); // your JSON library // 3. Poll until the build settles var settled = java.util.Set.of("healthy", "failed", "degraded"); String status = "pending"; while (!settled.contains(status)) { Thread.sleep(10_000); String body = apiPost("/api/applications/status", String.format("{"applicationId":"%s"}", appId)); status = jsonField(body, "status"); } if (!status.equals("healthy")) { throw new IllegalStateException("Build " + status); } ``` There is no `targetOrganisationId` anywhere above: a key carries its own organisation. With a session token every body would need it. ## Deploying from CI The case API keys exist for. Store the key as a repository secret, never in the workflow file. ```yaml # .github/workflows/deploy.yml name: Deploy to Light Cloud on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - name: Trigger deploy env: LIGHT_CLOUD_API_KEY: ${{ secrets.LIGHT_CLOUD_API_KEY }} APP_ID: ${{ vars.LIGHT_CLOUD_APP_ID }} run: | curl -sS --fail-with-body \ -X POST https://api.light-cloud.com/api/applications/deploy \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"applicationId\":\"$APP_ID\"}" ``` > [!TIP] > If the repository is connected through the [GitHub App](/create/git-automation), pushes already deploy on their own and this workflow is redundant. Use the API when the trigger is something else - a release, a manual approval, a green run in another pipeline. ## Organisation scope A key belongs to one organisation, so calls made with one need nothing extra - `targetOrganisationId` is inferred, and passing a different organisation's id is refused. Sessions are not scoped that way. A person can belong to several organisations, so a call authenticated with a session token must name the one it means: ```json { "targetOrganisationId": "org_...", "limit": 100 } ``` Read your organisations from the profile endpoint: ```bash curl https://api.light-cloud.com/api/auth/profile \ -H "Authorization: Bearer $TOKEN" ``` ```json { "id": "usr_...", "email": "you@example.com", "organisations": [ { "id": "org_...", "name": "Acme Labs", "slug": "acme-labs", "role": "owner" } ] } ``` Your role - or the key's access level - decides what the API lets you do. Each endpoint below names the permission it checks; see [Errors](/reference/api/errors#permissions) for the list. ## POST, mostly The API is RPC-shaped rather than REST-shaped. Reads and writes are both `POST` with a JSON body, and the verb lives in the path: ``` POST /api/applications list POST /api/applications/get read one POST /api/applications/create create POST /api/applications/delete delete ``` `GET` is used only where there is no body to send - the profile, the GitHub integration lookups, and [platform config](#platform-config). There is no `PATCH`, and `PUT` appears only under `/api/profile`. > [!TIP] > Resource ids are opaque strings. Never build one; always read it back from a list or create call. ## Responses A successful call returns the resource itself, unwrapped: ```json { "id": "app_...", "name": "shop", "status": "healthy" } ``` List endpoints that paginate return an envelope: ```json { "items": [], "totalItems": 0, "totalPages": 0, "currentPage": 1 } ``` Failures return an HTTP error status and a `message`, sometimes with a `code`: ```json { "message": "Organisation ID is required." } ``` See [Errors](/reference/api/errors) for the status codes and what to do about each. ## Pagination Endpoints that return an envelope accept these in the body: | Field | Type | Default | Range | | --- | --- | --- | --- | | `page` | number | `1` | 1-100000 | | `limit` | number | `10` | 1-100 | | `filter` | string | none | free text, matched against the name | | `sortColumn` | string | `created_at` | | | `sortOrder` | string | `desc` | `asc` or `desc` | Out-of-range values are clamped to the nearest legal one rather than rejected. ## Rate limits 5000 requests per 10 minutes per IP, across all endpoints. Standard `RateLimit-*` headers come back on every response; a breach is a `429`. Sign-in and MFA verification are limited more tightly and answer `429` with a `Retry-After` header in seconds. ## CORS Requests with no `Origin` - curl, CI runners, servers, CLIs - are always allowed. Browser requests are allowed only from origins on the platform allowlist, so a browser app you host yourself cannot call this API directly. Put your own backend in front of it. ## Health check ```endpoint GET /api/health > Whether the API is up. No authentication — useful as a reachability check before blaming your token. response 200 | Service is healthy { "status": "ok", "timestamp": "2026-08-28T13:31:20.268Z", "uptime": 5417.98 } code curl curl https://api.light-cloud.com/api/health code Node const res = await fetch("https://api.light-cloud.com/api/health"); const { status } = await res.json(); if (status !== "ok") throw new Error("Light Cloud API is not healthy"); code Python import requests res = requests.get("https://api.light-cloud.com/api/health", timeout=10) res.raise_for_status() if res.json()["status"] != "ok": raise RuntimeError("Light Cloud API is not healthy") ``` ## Platform catalogue Read these rather than hardcoding sizes, regions or tiers. The [Limits](/reference/limits) page is written from the same source, but the endpoint is the one that stays current. ```endpoint GET /api/config/platform > Regions, container sizes, database tiers and the rest of the catalogue the console fills its pickers from. response 200 | Catalogue values { "regions": [ { "id": "europe-west1", "label": "Belgium", "tier": 1 }, { "id": "us-central1", "label": "Iowa", "tier": 1 } ], "containerSizes": [ { "id": "micro", "vcpu": 1, "memory": "512Mi", "maxConcurrency": 80 } ] } code curl curl https://api.light-cloud.com/api/config/platform \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" code Node const res = await fetch("https://api.light-cloud.com/api/config/platform", { headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}` }, }); const { regions, containerSizes } = await res.json(); code Python import os import requests res = requests.get( "https://api.light-cloud.com/api/config/platform", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, ) res.raise_for_status() catalogue = res.json() ``` ```endpoint GET /api/config/feature-flags > Which optional features are on for your account. Some endpoints — the data explorer and database dumps — answer with a "not enabled" message when their flag is off, and this is where you check. response 200 | Flags and their values { "flags": ["database.shared-pool", "console.github-deploy-feedback"], "map": { "database.shared-pool": true, "console.gitlab": true, "console.bitbucket": true } } code curl curl https://api.light-cloud.com/api/config/feature-flags \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" code Node const res = await fetch("https://api.light-cloud.com/api/config/feature-flags", { headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}` }, }); const { map } = await res.json(); if (!map["database.shared-pool"]) { console.log("Shared database pool is off for this account"); } code Python import os import requests res = requests.get( "https://api.light-cloud.com/api/config/feature-flags", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, ) res.raise_for_status() flags = res.json()["map"] ``` ```endpoint GET /api/config/cloudrun > Container sizes and scaling ranges, with the memory and CPU combinations that are actually valid together. response 200 | Container catalogue { "sizes": [ { "id": "micro", "memory": "512Mi", "cpu": "1", "maxConcurrency": 80 } ], "scaling": { "minInstances": [0, 5], "maxInstances": [1, 100] } } code curl curl https://api.light-cloud.com/api/config/cloudrun \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" ``` There is also `GET /api/config/cloudrun/compatible?memory=`, which narrows the CPU options to those valid for a given memory setting. ## Reference - [Authentication](/reference/api/authentication): Sign in, refresh, and where tokens come from. - [Applications](/reference/api/applications): Create, deploy, rename, move, delete, and domains. - [Environments](/reference/api/environments): Branches, variables, scaling, logs, and metrics. - [Deployments](/reference/api/deployments): Deploy history and rollback. - [Databases](/reference/api/databases): Provision, connect, rotate, dump, and inspect. - [Uploads](/reference/api/uploads): Deploy a folder without a Git repository. - [GitHub](/reference/api/github): Installations, repositories, and branches. - [Errors](/reference/api/errors): Status codes, error codes, and permissions. --- # Authentication Every endpoint except [health](/reference/api#health-check) needs a credential. There are two, for two situations: | | Use it for | Available on | | --- | --- | --- | | **API key** | Scripts, CI, servers - anything running with nobody watching | Paid plans | | **Session token** | The [MCP server](/deploy-with-ai/mcp-server), the [CLI](/deploy-with-ai/cli), the console - anything with a person present (a browser, or a phone to approve a device code on) | Every plan, including free | If a browser can open, you do not need a key. If one cannot, a key is the only way in. ## Using a key ``` Authorization: Bearer lc_... ``` No refreshing and no expiry to handle unless you set one. A key belongs to one organisation, so `targetOrganisationId` is optional on every call - it is inferred, and naming a different organisation is refused with `CA003`. ```codetabs ### curl curl -X POST https://api.light-cloud.com/api/applications \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"limit": 100}' ### Node const res = await fetch("https://api.light-cloud.com/api/applications", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ limit: 100 }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); console.log(await res.json()); ### Python import os import requests res = requests.post( "https://api.light-cloud.com/api/applications", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"limit": 100}, ) res.raise_for_status() print(res.json()) ``` ## What a key cannot do By design, not by configuration: - **Billing and organisation settings.** No key, at any access level, can change a payment method, invite a member, or delete a workspace. Those need a signed-in person. - **Manage keys.** The endpoints below are session-only. A leaked key cannot mint a replacement or widen its own access - not even its own listing. - **Reach another workspace.** A key is bound to the organisation it was created in. > [!WARNING] > A key is a live credential for the whole workspace. Put it in your CI secret store, never in a repository, and never in a client-side bundle where a browser can read it. ## Plans Keys require a paid plan; creating one on a free plan returns `402`. The entitlement is re-checked on **every** request, so a downgrade stops existing keys working rather than letting them outlive it. This is a limit on unattended credentials, not on automation - the MCP server and the CLI work on every account and can do everything a key can, and more (a key can never touch billing). They just need a person to approve the initial sign-in, from any browser. --- ## Create an API key ```endpoint POST /api/api-keys/create > Issues a key and returns its secret. This is the only time the secret is ever readable - only a bcrypt hash is stored. Session tokens only. permission: update:organisations param targetOrganisationId | string | required | The organisation the key will belong to param name | string | required | What will use it, e.g. "GitHub Actions" param role | string | user | `user` for read-only, `admin` to deploy and manage. `owner` is not assignable param expiresAt | string | optional | RFC 3339 timestamp in the future. Omit for a key that never expires response 201 | The key, including its secret { "id": "key_3Wq8", "name": "GitHub Actions", "prefix": "lc_RdiJfPH9X", "role": "admin", "expires_at": "2027-08-28T00:00:00.000Z", "created_at": "2026-08-28T11:16:35.617Z", "secret": "lc_RdiJfPH9XmS2cA7vK1pQ0tZbN4eL8yG6uW3rD5oX9jH" } code curl curl -X POST https://api.light-cloud.com/api/api-keys/create \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "targetOrganisationId": "org_1Ab", "name": "GitHub Actions", "role": "admin", "expiresAt": "2027-08-28T00:00:00.000Z" }' code Node const res = await fetch("https://api.light-cloud.com/api/api-keys/create", { method: "POST", headers: { Authorization: `Bearer ${sessionToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ targetOrganisationId: "org_1Ab", name: "GitHub Actions", role: "admin", }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const key = await res.json(); // Store key.secret now - it cannot be read back console.log(key.secret); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/api-keys/create", headers={"Authorization": f"Bearer {session_token}"}, json={ "targetOrganisationId": "org_1Ab", "name": "GitHub Actions", "role": "admin", }, ) res.raise_for_status() key = res.json() # Store key["secret"] now - it cannot be read back print(key["secret"]) ``` > [!IMPORTANT] > `secret` appears in this response and nowhere else. If it is lost, revoke the key and create another. ## List API keys ```endpoint POST /api/api-keys > Every key in the organisation, newest first. Never returns the secret or its hash - `prefix` is the display form, enough to tell keys apart and useless on its own. Session tokens only. permission: read:organisations param targetOrganisationId | string | required | The organisation to read response 200 | Keys, without secrets [ { "id": "key_3Wq8", "name": "GitHub Actions", "prefix": "lc_RdiJfPH9X", "role": "admin", "created_by": "usr_5Kd", "expires_at": null, "revoked_at": null, "last_used_at": "2026-08-28T09:41:02.000Z", "created_at": "2026-08-20T10:00:00.000Z" } ] code curl curl -X POST https://api.light-cloud.com/api/api-keys \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{"targetOrganisationId": "org_1Ab"}' code Node const res = await fetch("https://api.light-cloud.com/api/api-keys", { method: "POST", headers: { Authorization: `Bearer ${sessionToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ targetOrganisationId: "org_1Ab" }), }); const keys = await res.json(); // Anything unused for 90 days is worth a look const stale = keys.filter( (key) => !key.revoked_at && (!key.last_used_at || Date.now() - Date.parse(key.last_used_at) > 90 * 864e5) ); code Python import os import requests from datetime import datetime, timedelta, timezone res = requests.post( "https://api.light-cloud.com/api/api-keys", headers={"Authorization": f"Bearer {session_token}"}, json={"targetOrganisationId": "org_1Ab"}, ) res.raise_for_status() cutoff = datetime.now(timezone.utc) - timedelta(days=90) stale = [ key for key in res.json() if not key["revoked_at"] and ( key["last_used_at"] is None or datetime.fromisoformat(key["last_used_at"].replace("Z", "+00:00")) < cutoff ) ] ``` ## Revoke an API key ```endpoint POST /api/api-keys/revoke > Stops a key working immediately - the next request with it is rejected. The row is kept rather than deleted, so the record of what existed survives. Session tokens only. permission: update:organisations param targetOrganisationId | string | required | The organisation the key belongs to param keyId | string | required | The key to revoke response 200 | Revoked { "revoked": true } code curl curl -X POST https://api.light-cloud.com/api/api-keys/revoke \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{"targetOrganisationId": "org_1Ab", "keyId": "key_3Wq8"}' code Node const res = await fetch("https://api.light-cloud.com/api/api-keys/revoke", { method: "POST", headers: { Authorization: `Bearer ${sessionToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ targetOrganisationId: "org_1Ab", keyId: "key_3Wq8", }), }); code Python import requests requests.post( "https://api.light-cloud.com/api/api-keys/revoke", headers={"Authorization": f"Bearer {session_token}"}, json={"targetOrganisationId": "org_1Ab", "keyId": "key_3Wq8"}, ).raise_for_status() ``` Revoke a key when the person who created it leaves, when the job it served is retired, or the moment you suspect it has leaked. A key without an expiry does not stop working on its own. > [!TIP] > Set an expiry on anything temporary. A key that outlives its purpose is the one that turns up in an old pipeline two years later. You can also do all of this in the console: **left sidebar → your workspace → API keys**. --- ## Session tokens The console, the MCP server and the CLI use short-lived tokens rather than keys: an access token valid for 15 minutes, plus a refresh token valid for 30 days. Sessions started from a terminal (the device-code flow) are listed under Settings → Security → Connected devices, where they can be revoked. Obtaining one happens in the browser, through the console. The MCP server does this for you; a person confirms the connection, and the tokens are written to `~/.lightcloud/credentials.json`. Implementing that flow yourself is not a supported integration path - use an API key. > [!NOTE] > `POST /api/auth/login` exists for the console. It expects the password encrypted with a platform secret that is not distributed, so it is not an authentication path for API clients. ## Refresh a session token ```endpoint POST /api/auth/refresh > Exchanges a refresh token for a new pair. The presented token is revoked in the same call, so the value changes every time - which is why a refresh token cannot live in a CI secret, and why API keys exist. param refreshToken | string | required | The current refresh token. Browsers may omit it and send the httpOnly cookie instead response 200 | A new pair { "token": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "9c1f4e2a7b..." } code curl curl -X POST https://api.light-cloud.com/api/auth/refresh \ -H "Content-Type: application/json" \ -H "X-Client-Type: cli" \ -d "{\"refreshToken\": \"$REFRESH_TOKEN\"}" code Node const res = await fetch("https://api.light-cloud.com/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json", // Without this the new refresh token comes back only as a cookie "X-Client-Type": "cli", }, body: JSON.stringify({ refreshToken }), }); if (!res.ok) throw new Error("Session expired, sign in again"); // Both values are new - store them, the old refresh token is now dead const { token, refreshToken: rotated } = await res.json(); code Python import requests res = requests.post( "https://api.light-cloud.com/api/auth/refresh", headers={"X-Client-Type": "cli"}, json={"refreshToken": refresh_token}, ) if not res.ok: raise RuntimeError("Session expired, sign in again") payload = res.json() # Both values are new - the old refresh token is now dead access_token, refresh_token = payload["token"], payload["refreshToken"] ``` > [!IMPORTANT] > `X-Client-Type: cli` is not optional outside a browser. Without it the response carries only a new access token, and your refresh token stays the old, now-revoked one. ## Retrieve the current user ```endpoint GET /api/auth/profile > The signed-in user and every organisation they belong to, with their role in each. This is where `targetOrganisationId` comes from. Session tokens only - a key has no user behind it. response 200 | The user and their organisations { "id": "usr_5Kd", "email": "you@example.com", "first_name": "Ada", "organisations": [ { "id": "org_1Ab", "name": "Acme Labs", "slug": "acme-labs", "role": "owner" } ] } code curl curl https://api.light-cloud.com/api/auth/profile \ -H "Authorization: Bearer $SESSION_TOKEN" code Node const res = await fetch("https://api.light-cloud.com/api/auth/profile", { headers: { Authorization: `Bearer ${sessionToken}` }, }); const { organisations } = await res.json(); const organisationId = organisations[0].id; code Python import requests res = requests.get( "https://api.light-cloud.com/api/auth/profile", headers={"Authorization": f"Bearer {session_token}"}, ) res.raise_for_status() organisation_id = res.json()["organisations"][0]["id"] ``` ## Sign out ```endpoint POST /api/auth/logout > Revokes the refresh token and clears the cookie. Access tokens already issued stay valid until they expire, up to 15 minutes later. response 200 | Signed out { "message": "Successfully logged out" } code curl curl -X POST https://api.light-cloud.com/api/auth/logout \ -H "Authorization: Bearer $SESSION_TOKEN" ``` ## Two-factor accounts When an account has two-factor authentication enabled, signing in returns a short-lived pending token instead of a session, and a TOTP or recovery code exchanges it for the real one at `POST /api/auth/login/mfa`. The console and MCP server handle this. API keys are unaffected - they are workspace credentials, not account credentials, and carry no second factor of their own. That is another reason they cannot touch billing or membership. ## Related - [API overview](/reference/api): Base URL, conventions, and rate limits. - [Errors](/reference/api/errors): What each status code means. - [MCP server](/deploy-with-ai/mcp-server): Deploy from an AI agent, on any plan. --- # Applications An application is one deployed project. Creating one also creates its production [environment](/reference/api/environments), so a single call gets you a live URL. With an [API key](/reference/api/authentication) the organisation is inferred. Session tokens must add `targetOrganisationId` to every body. ## List applications ```endpoint POST /api/applications > Applications in the organisation, newest first, paginated. permission: read:projects param page | number | 1 | 1-based page number param limit | number | 10 | Between 1 and 100 param filter | string | optional | Free text, matched against the name param sortColumn | string | created_at | Field to order by param sortOrder | string | desc | asc or desc param targetOrganisationId | string | session only | Required when using a session token response 200 | Paginated envelope { "items": [ { "id": "app_4kQ2", "name": "shop", "slug": "shop", "deployment_type": "container", "framework": "nextjs", "runtime": "nodejs", "github_repo_url": "https://github.com/acme/shop", "github_branch": "main", "source_type": "github", "status": "healthy", "url": "https://main-shop-acme.light-cloud.io", "created_at": "2026-08-01T09:14:00.000Z", "updated_at": "2026-08-28T07:02:00.000Z" } ], "totalItems": 1, "totalPages": 1, "currentPage": 1 } code curl curl -X POST https://api.light-cloud.com/api/applications \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"limit": 100}' code Node const response = await fetch("https://api.light-cloud.com/api/applications", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ limit: 100 }), }); if (!response.ok) throw new Error(await response.text()); const { items } = await response.json(); items.forEach((app) => console.log(app.name, app.status)); code Python import os import requests response = requests.post( "https://api.light-cloud.com/api/applications", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"limit": 100}, ) response.raise_for_status() for app in response.json()["items"]: print(app["name"], app["status"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com/api/applications")) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"limit\":100}")) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new IllegalStateException(response.body()); } System.out.println(response.body()); code Go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { body := bytes.NewBufferString(`{"limit":100}`) req, _ := http.NewRequest("POST", "https://api.light-cloud.com/api/applications", body) req.Header.Set("Authorization", "Bearer "+os.Getenv("LIGHT_CLOUD_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var page struct { Items []struct { Name string `json:"name"` Status string `json:"status"` } `json:"items"` } json.NewDecoder(res.Body).Decode(&page) for _, app := range page.Items { fmt.Println(app.Name, app.Status) } } ``` ## Retrieve an application ```endpoint POST /api/applications/get > One application with its environments. `status` is one of pending, building, deploying, healthy, degraded, failed, deleting. permission: read:projects param applicationId | string | required | The application to read param targetOrganisationId | string | session only | Required when using a session token response 200 | An application { "id": "app_4kQ2", "name": "shop", "status": "healthy", "url": "https://main-shop-acme.light-cloud.io", "environments": [ { "id": "env_7Tz", "name": "production", "is_production": true } ] } code curl curl -X POST https://api.light-cloud.com/api/applications/get \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const app = await api("/api/applications/get", { applicationId: "app_4kQ2", }); console.log(app.url); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() app = api("/api/applications/get", applicationId="app_4kQ2") print(app["url"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // Everything this does is visible: POST, bearer token, JSON in, JSON out. static String apiPost(String path, String json) throws Exception { var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com" + path)) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) throw new IllegalStateException(response.body()); return response.body(); } String response = apiPost( "/api/applications/get", "{\"applicationId\":\"app_4kQ2\"}"); ``` ## Retrieve status ```endpoint POST /api/applications/status > The same shape as retrieve, cheaper. Use it when polling a deploy rather than re-reading the whole record. permission: read:projects param applicationId | string | required | The application to poll param targetOrganisationId | string | session only | Required when using a session token response 200 | Current status { "id": "app_4kQ2", "status": "building", "url": "https://main-shop-acme.light-cloud.io" } code curl until [ "$(curl -sS -X POST https://api.light-cloud.com/api/applications/status \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"applicationId\":\"$APP_ID\"}" | jq -r .status)" = "healthy" ]; do sleep 10 done code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const settled = ["healthy", "failed", "degraded"]; let status = "pending"; while (!settled.includes(status)) { await new Promise((resolve) => setTimeout(resolve, 10_000)); ({ status } = await api("/api/applications/status", { applicationId: appId, })); } if (status !== "healthy") throw new Error(`Deploy ended ${status}`); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() import time SETTLED = {"healthy", "failed", "degraded"} status = "pending" while status not in SETTLED: time.sleep(10) status = api( "/api/applications/status", applicationId=app_id )["status"] if status != "healthy": raise RuntimeError(f"Deploy ended {status}") code Java var settled = java.util.Set.of("healthy", "failed", "degraded"); String status = "pending"; while (!settled.contains(status)) { Thread.sleep(10_000); status = readStatus(appId); } ``` ## Detect a framework ```endpoint POST /api/applications/detect-framework > Reads a repository the way the console's create page does and returns the settings it would fill in. Call this before create rather than guessing. permission: read:projects param owner | string | required | Repository owner param repo | string | required | Repository name param branch | string | required | Branch to inspect param rootDirectory | string | optional | Subdirectory, for a monorepo param targetOrganisationId | string | session only | Required when using a session token response 200 | Detected build settings { "framework": "nextjs", "runtime": "nodejs", "deploymentType": "container", "buildCommand": "npm run build", "outputDirectory": ".next" } code curl curl -X POST https://api.light-cloud.com/api/applications/detect-framework \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"owner": "acme", "repo": "shop", "branch": "main"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const detected = await api("/api/applications/detect-framework", { owner: "acme", repo: "shop", branch: "main", }); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() detected = api( "/api/applications/detect-framework", owner="acme", repo="shop", branch="main", ) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // Everything this does is visible: POST, bearer token, JSON in, JSON out. static String apiPost(String path, String json) throws Exception { var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com" + path)) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) throw new IllegalStateException(response.body()); return response.body(); } String detected = apiPost( "/api/applications/detect-framework", "{\"owner\":\"acme\",\"repo\":\"shop\",\"branch\":\"main\"}"); ``` ## Create an application ```endpoint POST /api/applications/create > Creates the application and its production environment, and starts the first build. Creates are not idempotent — if one times out, list before retrying. permission: create:projects param name | string | required | Becomes part of the generated subdomain param githubRepoUrl | string | required | Repository URL on GitHub, GitLab or Bitbucket - must be reachable param gitProvider | string | github | github, gitlab or bitbucket; a gitlab.com or bitbucket.org URL implies its provider param gitlabProjectId | number | resolved | GitLab only - the numeric project id; resolved from the URL when omitted param bitbucketRepoUuid | string | resolved | Bitbucket only - the repository uuid (braced); resolved from the URL when omitted param githubBranch | string | main | The production environment's branch param isPrivate | boolean | false | Private repositories need an installation (GitHub) or a connected GitLab or Bitbucket user param projectId | string | optional | Folder to file it under param deploymentType | string | static | static or container param framework | string | react | See Frameworks param runtime | string | optional | Container only param buildCommand | string | detected | Command that produces the build param outputDirectory | string | detected | Static only param rootDirectory | string | optional | Monorepo subdirectory param environmentVars | object | optional | String values only param containerPort | number | detected | Container only param memory | string | 512Mi | See Limits param cpu | string | optional | Container only param minInstances | number | 0 | 0 to 5 param maxInstances | number | 10 | 1 to 100, up to the plan limit param concurrency | number | 80 | Requests served per instance param cpuTarget | number | 80 | Percentage that triggers a scale-up param region | string | optional | See Limits param customDomain | string | optional | Can be added later param autoDeployOnPush | boolean | optional | Deploy on every push param autoDeployBranches | string[] | optional | Branches that auto-deploy param autoDeleteStaleEnvs | boolean | optional | Remove environments for deleted branches response 201 | The created application { "id": "app_4kQ2", "name": "shop", "status": "pending", "url": "https://main-shop-acme.light-cloud.io" } code curl curl -X POST https://api.light-cloud.com/api/applications/create \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "shop", "githubRepoUrl": "https://github.com/acme/shop", "githubBranch": "main", "deploymentType": "container", "framework": "nextjs", "runtime": "nodejs", "buildCommand": "npm run build", "environmentVars": { "NODE_ENV": "production" }, "memory": "512Mi", "maxInstances": 5 }' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const app = await api("/api/applications/create", { name: "shop", githubRepoUrl: "https://github.com/acme/shop", githubBranch: "main", deploymentType: "container", framework: "nextjs", runtime: "nodejs", buildCommand: "npm run build", environmentVars: { NODE_ENV: "production" }, memory: "512Mi", maxInstances: 5, }); console.log(app.id); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() app = api( "/api/applications/create", name="shop", githubRepoUrl="https://github.com/acme/shop", githubBranch="main", deploymentType="container", framework="nextjs", runtime="nodejs", buildCommand="npm run build", environmentVars={"NODE_ENV": "production"}, memory="512Mi", maxInstances=5, ) print(app["id"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // Everything this does is visible: POST, bearer token, JSON in, JSON out. static String apiPost(String path, String json) throws Exception { var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com" + path)) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) throw new IllegalStateException(response.body()); return response.body(); } var body = """ { "name": "shop", "githubRepoUrl": "https://github.com/acme/shop", "githubBranch": "main", "deploymentType": "container", "framework": "nextjs", "runtime": "nodejs", "buildCommand": "npm run build", "environmentVars": { "NODE_ENV": "production" }, "memory": "512Mi", "maxInstances": 5 } """; String created = apiPost("/api/applications/create", body); ``` ## Create from an upload ```endpoint POST /api/applications/create-from-upload > Builds an application from an archive instead of a repository. Nothing auto-deploys afterwards, since there is no repository to watch. permission: create:projects param name | string | required | Becomes part of the generated subdomain param uploadId | string | required | From the uploads flow param deploymentType | string | required | static or container param projectId | string | optional | Folder to file it under param framework | string | optional | Overrides detection param runtime | string | optional | Container only param buildCommand | string | optional | Command that produces the build param outputDirectory | string | optional | Static only param startCommand | string | optional | Container only param environmentVars | object | optional | String values only response 201 | The created application { "id": "app_9Lm3", "name": "marketing-site", "source_type": "upload", "status": "pending" } code curl curl -X POST https://api.light-cloud.com/api/applications/create-from-upload \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "marketing-site", "uploadId": "upl_8Xa1", "deploymentType": "static", "outputDirectory": "." }' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const app = await api("/api/applications/create-from-upload", { name: "marketing-site", uploadId, deploymentType: "static", outputDirectory: ".", }); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() app = api( "/api/applications/create-from-upload", name="marketing-site", uploadId=upload_id, deploymentType="static", outputDirectory=".", ) ``` ## Deploy ```endpoint POST /api/applications/deploy > Builds the current head of the environment's branch and releases it. Returns as soon as the deployment starts — poll status for the outcome. permission: update:projects param applicationId | string | required | The application to deploy param environmentId | string | optional | Defaults to production param targetOrganisationId | string | session only | Required when using a session token response 200 | The deployment that just started { "id": "dep_9f2a", "environment_id": "env_7Tz", "status": "pending", "commit_sha": "9f3c1ab", "started_at": "2026-08-28T07:00:12.000Z" } code curl curl -X POST https://api.light-cloud.com/api/applications/deploy \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const deployment = await api("/api/applications/deploy", { applicationId: "app_4kQ2", }); console.log("started", deployment.id); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() deployment = api( "/api/applications/deploy", applicationId="app_4kQ2" ) print("started", deployment["id"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; // Everything this does is visible: POST, bearer token, JSON in, JSON out. static String apiPost(String path, String json) throws Exception { var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com" + path)) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) throw new IllegalStateException(response.body()); return response.body(); } String deployment = apiPost( "/api/applications/deploy", "{\"applicationId\":\"app_4kQ2\"}"); ``` ## Rename ```endpoint POST /api/applications/rename > Renaming changes the generated subdomain, and the old URL stops resolving. permission: update:projects param applicationId | string | required | The application to rename param name | string | required | The new name response 200 | The updated application { "id": "app_4kQ2", "name": "storefront", "url": "https://main-storefront-acme.light-cloud.io" } code curl curl -X POST https://api.light-cloud.com/api/applications/rename \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2", "name": "storefront"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; await api("/api/applications/rename", { applicationId: "app_4kQ2", name: "storefront", }); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() api( "/api/applications/rename", applicationId="app_4kQ2", name="storefront", ) ``` ## Move ```endpoint POST /api/applications/move > Files the application under a different folder. permission: update:projects param applicationId | string | required | The application to move param targetFolderId | string | required | Folder id, or null for the root response 200 | The updated application { "id": "app_4kQ2", "project_id": "prj_2Bd" } code curl curl -X POST https://api.light-cloud.com/api/applications/move \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2", "targetFolderId": "prj_2Bd"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; await api("/api/applications/move", { applicationId: "app_4kQ2", targetFolderId: null, // root }); ``` ## Delete ```endpoint DELETE /api/applications/delete > Tears down every environment. Not reversible, and issued as a POST despite the verb shown here. permission: delete:projects param applicationId | string | required | The application to delete param targetOrganisationId | string | session only | Required when using a session token response 200 | Deletion accepted { "message": "Application deleted" } code curl curl -X POST https://api.light-cloud.com/api/applications/delete \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; await api("/api/applications/delete", { applicationId: "app_4kQ2", }); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() api("/api/applications/delete", applicationId="app_4kQ2") ``` ## Custom domains Each of these takes either `applicationId` (which targets the production environment) or `environmentId`. See [Custom domains](/platform/custom-domains) for the DNS records to create. ```endpoint POST /api/applications/add-domain > Attaches a domain and starts certificate issuance. Certificates are issued asynchronously — poll check-domain for progress. permission: update:projects param applicationId | string | one of two | Targets the production environment param environmentId | string | one of two | Targets a specific environment param domain | string | required | The hostname to attach response 200 | Domain attached, verification pending { "domain": "shop.example.com", "dns_verified": false, "certificate_status": "provisioning" } code curl curl -X POST https://api.light-cloud.com/api/applications/add-domain \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2", "domain": "shop.example.com"}' code Node const API = "https://api.light-cloud.com"; // Everything this does is visible: POST, bearer token, JSON in, JSON out. const api = async (path, body = {}) => { const res = await fetch(API + path, { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; await api("/api/applications/add-domain", { applicationId: "app_4kQ2", domain: "shop.example.com", }); const state = await api("/api/applications/check-domain", { applicationId: "app_4kQ2", }); code Python import os import requests API = "https://api.light-cloud.com" # Everything this does is visible: POST, bearer token, JSON in, JSON out. def api(path, **body): res = requests.post( API + path, headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json=body, ) res.raise_for_status() return res.json() api( "/api/applications/add-domain", applicationId="app_4kQ2", domain="shop.example.com", ) state = api( "/api/applications/check-domain", applicationId="app_4kQ2" ) ``` | Endpoint | Extra parameters | Description | | --- | --- | --- | | `POST /api/applications/check-domain` | | Reports DNS and certificate state | | `POST /api/applications/update-domain` | `newDomain` | Replaces the attached domain | | `POST /api/applications/retry-domain` | | Retries a failed verification | | `POST /api/applications/remove-domain` | `revertToAutoSubdomain` (default `true`) | Detaches it | ## Repository helpers | Endpoint | Description | | --- | --- | | `POST /api/applications/list-repo-directories` | Folders in a repository, for root-directory pickers | | `POST /api/applications/public-branches` | Branches of a public repository, no installation needed | | `GET /api/applications/lookup/:subdomain` | Which application owns a subdomain | ## Related - [Environments](/reference/api/environments): Branches, variables, scaling, and logs. - [Deployments](/reference/api/deployments): History and rollback. - [GitHub](/reference/api/github): Making a private repository reachable. --- # Environments An environment is one branch of one application, with its own URL, variables, and scaling. Every application has a production environment from birth; the rest you create. With an [API key](/reference/api/authentication) the organisation is inferred. Session tokens must add `targetOrganisationId` to every body. ## List environments ```endpoint POST /api/environments > Every environment of one application. Returns an array, not a paginated envelope. permission: read:projects param applicationId | string | required | The application to read param targetOrganisationId | string | session only | Required when using a session token response 200 | Environments [ { "id": "env_7Tz", "application_id": "app_4kQ2", "name": "production", "github_branch": "main", "is_production": true, "status": "healthy", "url": "https://main-shop-acme.light-cloud.io", "custom_domain": "shop.example.com", "created_at": "2026-08-01T09:14:00.000Z", "updated_at": "2026-08-28T07:02:00.000Z" } ] code curl curl -X POST https://api.light-cloud.com/api/environments \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"applicationId": "app_4kQ2"}' code Node const res = await fetch("https://api.light-cloud.com/api/environments", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ applicationId: "app_4kQ2" }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const environments = await res.json(); environments.forEach((e) => console.log(e.name, e.github_branch, e.url)); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/environments", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"applicationId": "app_4kQ2"}, ) res.raise_for_status() for e in res.json(): print(e["name"], e["github_branch"], e["url"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com/api/environments")) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"applicationId\":\"app_4kQ2\"}")) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ## Retrieve an environment ```endpoint POST /api/environments/get > One environment, including its `environment_vars`. permission: read:projects param environmentId | string | required | The environment to read param targetOrganisationId | string | session only | Required when using a session token response 200 | An environment { "id": "env_7Tz", "application_id": "app_4kQ2", "name": "production", "github_branch": "main", "is_production": true, "status": "healthy", "url": "https://main-shop-acme.light-cloud.io", "environment_vars": { "NODE_ENV": "production" }, "min_instances": 0, "max_instances": 5 } code curl curl -X POST https://api.light-cloud.com/api/environments/get \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_7Tz"}' code Node const res = await fetch("https://api.light-cloud.com/api/environments/get", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz" }), }); const environment = await res.json(); console.log(environment.environment_vars); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/environments/get", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"environmentId": "env_7Tz"}, ) res.raise_for_status() print(res.json()["environment_vars"]) ``` ## Retrieve status ```endpoint POST /api/environments/status > Cheaper than retrieve. Poll this while a deploy runs. permission: read:projects param environmentId | string | required | The environment to poll param targetOrganisationId | string | session only | Required when using a session token response 200 | Current status { "id": "env_7Tz", "status": "deploying" } code curl until [ "$(curl -sS -X POST https://api.light-cloud.com/api/environments/status \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"environmentId\":\"$ENV_ID\"}" | jq -r .status)" = "healthy" ]; do sleep 10 done code Node const settled = ["healthy", "failed", "degraded"]; let status = "pending"; while (!settled.includes(status)) { await new Promise((resolve) => setTimeout(resolve, 10_000)); const res = await fetch( "https://api.light-cloud.com/api/environments/status", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId }), } ); ({ status } = await res.json()); } if (status !== "healthy") throw new Error(`Deploy ended ${status}`); code Python import os import time import requests SETTLED = {"healthy", "failed", "degraded"} headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} status = "pending" while status not in SETTLED: time.sleep(10) res = requests.post( "https://api.light-cloud.com/api/environments/status", headers=headers, json={"environmentId": environment_id}, ) res.raise_for_status() status = res.json()["status"] if status != "healthy": raise RuntimeError(f"Deploy ended {status}") ``` ## Create an environment ```endpoint POST /api/environments/create > Adds a branch environment to an application. See preview environments for what the platform creates on its own. permission: create:projects param applicationId | string | required | The application to add it to param name | string | required | Becomes part of the URL param githubBranch | string | required | The branch this environment tracks param isProduction | boolean | false | Whether this is the production environment param autoDeploy | boolean | optional | Deploy on every push to the branch param buildCommand | string | inherited | Falls back to the application's param outputDirectory | string | inherited | Falls back to the application's param environmentVars | object | optional | String values only param containerPort | number | optional | Container only param memory | string | optional | Container only param cpu | string | optional | Container only param minInstances | number | 0 | 0 to 5 param maxInstances | number | 10 | 1 to 100, up to the plan limit param customDomain | string | optional | Can be added later response 201 | The created environment { "id": "env_2Bd", "application_id": "app_4kQ2", "name": "staging", "github_branch": "develop", "is_production": false, "status": "pending" } code curl curl -X POST https://api.light-cloud.com/api/environments/create \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "applicationId": "app_4kQ2", "name": "staging", "githubBranch": "develop", "autoDeploy": true, "environmentVars": { "NODE_ENV": "staging" } }' code Node const res = await fetch("https://api.light-cloud.com/api/environments/create", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ applicationId: "app_4kQ2", name: "staging", githubBranch: "develop", autoDeploy: true, environmentVars: { NODE_ENV: "staging" }, }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const environment = await res.json(); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/environments/create", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={ "applicationId": "app_4kQ2", "name": "staging", "githubBranch": "develop", "autoDeploy": True, "environmentVars": {"NODE_ENV": "staging"}, }, ) res.raise_for_status() environment = res.json() ``` ## Update an environment ```endpoint POST /api/environments/update > Changes settings or variables. Variables take effect on the next build, so deploy afterwards. permission: update:projects param environmentId | string | required | The environment to update param name | string | optional | Renaming changes the URL param buildCommand | string | optional | Command that produces the build param outputDirectory | string | optional | Static only param environmentVars | object | optional | Replaces the whole set — see the warning below param containerPort | number | optional | Container only param memory | string | optional | Container only param cpu | string | optional | Container only param minInstances | number | optional | 0 to 5 param maxInstances | number | optional | 1 to 100, up to the plan limit param region | string | optional | See Limits param customDomain | string | optional | Attached hostname param autoDeploy | boolean | optional | Deploy on every push response 200 | The updated environment { "id": "env_7Tz", "environment_vars": { "NODE_ENV": "production", "FEATURE_CHECKOUT_V2": "true" } } code curl API=https://api.light-cloud.com AUTH=(-H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" -H "Content-Type: application/json") # Read, merge, write — sending a partial map deletes the rest current=$(curl -sS -X POST "$API/api/environments/get" "${AUTH[@]}" \ -d "{\"environmentId\":\"$ENV_ID\"}") vars=$(echo "$current" | jq '.environment_vars + {"FEATURE_CHECKOUT_V2":"true"}') curl -sS -X POST "$API/api/environments/update" "${AUTH[@]}" \ -d "{\"environmentId\":\"$ENV_ID\",\"environmentVars\":$vars}" curl -sS -X POST "$API/api/environments/deploy" "${AUTH[@]}" \ -d "{\"environmentId\":\"$ENV_ID\"}" code Node const API = "https://api.light-cloud.com"; const headers = { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }; const post = async (path, body) => { const res = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; // Read, merge, write — sending a partial map deletes the rest const current = await post("/api/environments/get", { environmentId }); await post("/api/environments/update", { environmentId, environmentVars: { ...current.environment_vars, FEATURE_CHECKOUT_V2: "true", }, }); await post("/api/environments/deploy", { environmentId }); code Python import os import requests API = "https://api.light-cloud.com" headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} def post(path, **body): res = requests.post(API + path, headers=headers, json=body) res.raise_for_status() return res.json() # Read, merge, write — sending a partial map deletes the rest current = post("/api/environments/get", environmentId=environment_id) post( "/api/environments/update", environmentId=environment_id, environmentVars={**current["environment_vars"], "FEATURE_CHECKOUT_V2": "true"}, ) post("/api/environments/deploy", environmentId=environment_id) ``` > [!WARNING] > `environmentVars` replaces the whole set, it does not merge. Send only the keys you want and every variable you left out is deleted. Read the environment first, merge, then send it all back. ## Scale ```endpoint POST /api/environments/scale > Changes the instance range without a rebuild. Sending neither bound is a 400. permission: update:projects param environmentId | string | required | The environment to scale param minInstances | number | one of two | 0 to 5. Above 0 keeps instances warm param maxInstances | number | one of two | 1 to 100, up to the plan limit param targetOrganisationId | string | session only | Required when using a session token response 200 | The updated environment { "id": "env_7Tz", "min_instances": 1, "max_instances": 20 } code curl curl -X POST https://api.light-cloud.com/api/environments/scale \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_7Tz", "minInstances": 1, "maxInstances": 20}' code Node // Keep one instance warm before a launch const res = await fetch("https://api.light-cloud.com/api/environments/scale", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz", minInstances: 1, maxInstances: 20, }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); code Python import os import requests requests.post( "https://api.light-cloud.com/api/environments/scale", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"environmentId": "env_7Tz", "minInstances": 1, "maxInstances": 20}, ).raise_for_status() ``` ## Deploy ```endpoint POST /api/environments/deploy > Builds the environment's branch at its current head and releases it. Returns as soon as the deployment starts. permission: update:projects param environmentId | string | required | The environment to deploy param targetOrganisationId | string | session only | Required when using a session token response 200 | The deployment that just started { "id": "dep_9f2a", "environment_id": "env_7Tz", "status": "pending" } code curl curl -X POST https://api.light-cloud.com/api/environments/deploy \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_7Tz"}' code Node const res = await fetch("https://api.light-cloud.com/api/environments/deploy", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz" }), }); const deployment = await res.json(); console.log("started", deployment.id); ``` ## Delete an environment ```endpoint POST /api/environments/delete > Removes the environment and everything running in it. The production environment goes only with its application. permission: delete:projects param environmentId | string | required | The environment to delete param targetOrganisationId | string | session only | Required when using a session token response 200 | Deletion accepted { "message": "Environment deleted" } code curl curl -X POST https://api.light-cloud.com/api/environments/delete \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_2Bd"}' ``` ## Read logs ```endpoint POST /api/environments/logs > Runtime logs, kept 7 days. Returns an array of log lines. permission: read:projects param environmentId | string | required | The environment to read param filters.startTime | string | optional | RFC 3339 timestamp param filters.endTime | string | optional | RFC 3339 timestamp param filters.severity | string[] | optional | Severity names, OR-ed together param filters.textSearch | string | optional | Substring match on the message param targetOrganisationId | string | session only | Required when using a session token response 200 | Log lines [ "2026-08-28T09:41:02Z ERROR checkout: payment intent expired", "2026-08-28T09:41:02Z ERROR checkout: returning 502 to client" ] code curl curl -X POST https://api.light-cloud.com/api/environments/logs \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"environmentId\": \"env_7Tz\", \"filters\": { \"startTime\": \"$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)\", \"severity\": [\"ERROR\", \"CRITICAL\"], \"textSearch\": \"checkout\" } }" code Node const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const res = await fetch("https://api.light-cloud.com/api/environments/logs", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz", filters: { startTime: oneHourAgo, severity: ["ERROR", "CRITICAL"], textSearch: "checkout", }, }), }); const lines = await res.json(); console.log(lines.join("\n")); code Python import os import requests from datetime import datetime, timedelta, timezone one_hour_ago = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat() res = requests.post( "https://api.light-cloud.com/api/environments/logs", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={ "environmentId": "env_7Tz", "filters": { "startTime": one_hour_ago, "severity": ["ERROR", "CRITICAL"], "textSearch": "checkout", }, }, ) res.raise_for_status() print("\n".join(res.json())) ``` For a live tail there is a server-sent events stream: ``` GET /api/environments/:targetOrganisationId/:environmentId/logs/stream ``` ## Read detailed metrics ```endpoint POST /api/environments/metrics/detailed > Time series for one environment. Omitting `metrics` returns all of them. permission: read:projects param environmentId | string | required | The environment to measure param timeRange | string | required | `1h`, `6h`, `24h` or `7d` param metrics | string[] | optional | `cpu`, `memory`, `requestCount`, `latency`, `instanceCount`, `bandwidthOut`, `bandwidthIn` param targetOrganisationId | string | session only | Required when using a session token response 200 | Series per metric { "environmentId": "env_7Tz", "deploymentType": "container", "timeRange": { "startTime": "2026-08-27T09:00:00.000Z", "endTime": "2026-08-28T09:00:00.000Z" }, "metrics": { "cpu": [{ "t": "2026-08-28T08:00:00.000Z", "v": 0.34 }], "requestCount": [{ "t": "2026-08-28T08:00:00.000Z", "v": 1284 }] } } code curl curl -X POST https://api.light-cloud.com/api/environments/metrics/detailed \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "environmentId": "env_7Tz", "timeRange": "24h", "metrics": ["cpu", "memory", "requestCount"] }' code Node const res = await fetch( "https://api.light-cloud.com/api/environments/metrics/detailed", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz", timeRange: "24h", metrics: ["cpu", "memory", "requestCount"], }), } ); const { metrics } = await res.json(); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/environments/metrics/detailed", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={ "environmentId": "env_7Tz", "timeRange": "24h", "metrics": ["cpu", "memory", "requestCount"], }, ) res.raise_for_status() metrics = res.json()["metrics"] ``` There is also `POST /api/environments/metrics/sparkline`, which takes `environmentIds` (an array) and returns small series for several environments at once, for list views. ## Read activity ```endpoint POST /api/environments/activity > Settings and variable changes, without the values. permission: read:projects param environmentId | string | required | The environment to read param limit | number | optional | How many entries to return param offset | number | optional | Skip this many response 200 | Activity entries [ { "id": "act_5Wq", "action": "environment_vars_updated", "actor_email": "you@example.com", "created_at": "2026-08-28T07:00:00.000Z" } ] code curl curl -X POST https://api.light-cloud.com/api/environments/activity \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_7Tz", "limit": 20}' ``` ## Password protection ```endpoint POST /api/environments/password > Puts a password wall in front of the environment's URL. `enabled` must be a boolean, not a string. permission: update:projects param environmentId | string | required | The environment to protect param enabled | boolean | required | Turns the wall on or off param password | string | when enabling | The password visitors must enter response 200 | Protection state { "id": "env_2Bd", "password_protected": true } code curl curl -X POST https://api.light-cloud.com/api/environments/password \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_2Bd", "enabled": true, "password": "preview-only"}' code Node const res = await fetch("https://api.light-cloud.com/api/environments/password", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_2Bd", enabled: true, password: "preview-only", }), }); ``` Visitors exchange the password for access at `POST /api/environments/unlock`, with body `environmentId` and `password`. That endpoint takes no bearer token — it is what the wall itself calls, and it is rate limited per address. ## Domains `check-domain`, `add-domain`, and `retry-domain` also exist under `/api/environments`, with the same bodies as their [application](/reference/api/applications#custom-domains) counterparts, addressed by `environmentId`. ## Related - [Applications](/reference/api/applications): The parent resource. - [Deployments](/reference/api/deployments): What a deploy produced. - [Environment variables](/platform/environment-variables): Build-time versus runtime. --- # Deployments A deployment is one commit, built and released to one environment. Each environment keeps its last 20; the last 10 successful ones stay available as rollback targets. With an [API key](/reference/api/authentication) the organisation is inferred. Session tokens must add `targetOrganisationId` to every body. ## List deployments ```endpoint POST /api/deployments > An environment's deploy history, newest first. permission: read:projects param environmentId | string | required | The environment to read param limit | number | 20 | How many to return param offset | number | 0 | Skip this many param targetOrganisationId | string | session only | Required when using a session token response 200 | Deployments, newest first [ { "id": "dep_9f2a", "environment_id": "env_7Tz", "status": "healthy", "deployment_stage": "released", "commit_sha": "9f3c1ab", "commit_message": "Fix cart total rounding", "started_at": "2026-08-28T07:00:12.000Z", "completed_at": "2026-08-28T07:02:40.000Z" }, { "id": "dep_7c11", "environment_id": "env_7Tz", "status": "failed", "commit_sha": "2b8e004", "commit_message": "Bump image deps", "started_at": "2026-08-27T16:41:03.000Z", "completed_at": "2026-08-27T16:43:55.000Z" } ] code curl curl -X POST https://api.light-cloud.com/api/deployments \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"environmentId": "env_7Tz", "limit": 5}' code Node const res = await fetch("https://api.light-cloud.com/api/deployments", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ environmentId: "env_7Tz", limit: 5 }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const deployments = await res.json(); deployments.forEach((d) => console.log(d.commit_sha, d.status, d.commit_message)); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/deployments", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"environmentId": "env_7Tz", "limit": 5}, ) res.raise_for_status() for d in res.json(): print(d["commit_sha"], d["status"], d["commit_message"]) code Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; var request = HttpRequest.newBuilder() .uri(URI.create("https://api.light-cloud.com/api/deployments")) .header("Authorization", "Bearer " + System.getenv("LIGHT_CLOUD_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"environmentId\":\"env_7Tz\",\"limit\":5}")) .build(); var response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); ``` ## Retrieve a deployment ```endpoint POST /api/deployments/get > One deployment with its build logs. `status` moves through pending, building, deploying, then settles on healthy or failed; `deployment_stage` is the finer step within that. permission: read:projects param deploymentId | string | required | The deployment to read param targetOrganisationId | string | session only | Required when using a session token response 200 | A deployment { "id": "dep_9f2a", "environment_id": "env_7Tz", "status": "healthy", "deployment_stage": "released", "commit_sha": "9f3c1ab", "commit_message": "Fix cart total rounding", "started_at": "2026-08-28T07:00:12.000Z", "completed_at": "2026-08-28T07:02:40.000Z", "deployment_logs": [ "Building container image...", "Pushing to registry...", "Released to production" ] } code curl curl -X POST https://api.light-cloud.com/api/deployments/get \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"deploymentId": "dep_9f2a"}' code Node const res = await fetch("https://api.light-cloud.com/api/deployments/get", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ deploymentId: "dep_9f2a" }), }); const deployment = await res.json(); console.log(deployment.deployment_logs.join("\n")); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/deployments/get", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"deploymentId": "dep_9f2a"}, ) res.raise_for_status() print("\n".join(res.json()["deployment_logs"])) ``` > [!TIP] > Poll [environment status](/reference/api/environments#retrieve-status) rather than this endpoint while a deploy runs. It is cheaper, and it is what the console does. ## Roll back ```endpoint POST /api/deployments/rollback > Re-releases an earlier deployment's existing artifact. Nothing is rebuilt, so it takes seconds and the result is byte-identical to what ran before. Only the last 10 successful deployments are eligible. permission: update:projects param environmentId | string | required | The environment to roll back param deploymentId | string | required | The deployment to return to param targetOrganisationId | string | session only | Required when using a session token response 200 | The re-released deployment { "id": "dep_7c11", "environment_id": "env_7Tz", "status": "deploying", "commit_sha": "2b8e004" } code curl API=https://api.light-cloud.com AUTH=(-H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" -H "Content-Type: application/json") # The newest healthy deployment that is not the one currently released last_good=$(curl -sS -X POST "$API/api/deployments" "${AUTH[@]}" \ -d "{\"environmentId\":\"$ENV_ID\",\"limit\":10}" \ | jq -r '[.[] | select(.status == "healthy")][1].id') curl -sS -X POST "$API/api/deployments/rollback" "${AUTH[@]}" \ -d "{\"environmentId\":\"$ENV_ID\",\"deploymentId\":\"$last_good\"}" code Node const API = "https://api.light-cloud.com"; const headers = { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }; const historyRes = await fetch(`${API}/api/deployments`, { method: "POST", headers, body: JSON.stringify({ environmentId, limit: 10 }), }); const history = await historyRes.json(); // [0] is what is running now, so take the next healthy one const lastGood = history.filter((d) => d.status === "healthy")[1]; if (!lastGood) throw new Error("No earlier healthy deployment to roll back to"); await fetch(`${API}/api/deployments/rollback`, { method: "POST", headers, body: JSON.stringify({ environmentId, deploymentId: lastGood.id }), }); code Python import os import requests API = "https://api.light-cloud.com" headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} history = requests.post( f"{API}/api/deployments", headers=headers, json={"environmentId": environment_id, "limit": 10}, ).json() healthy = [d for d in history if d["status"] == "healthy"] if len(healthy) < 2: raise RuntimeError("No earlier healthy deployment to roll back to") requests.post( f"{API}/api/deployments/rollback", headers=headers, json={"environmentId": environment_id, "deploymentId": healthy[1]["id"]}, ).raise_for_status() ``` **Errors** | Status | Cause | | --- | --- | | `400` | The deployment is older than the rollback window, or never succeeded | ```json { "message": "This deployment can no longer be rolled back to. Only the last 10 successful deployments are kept as rollback targets." } ``` > [!NOTE] > A rollback does not change which branch the environment tracks. The next push to that branch deploys forwards again, over the rollback. ## Related - [Environments](/reference/api/environments): Where deployments happen. - [Deployments](/platform/deployments): How builds and releases work. - [Observability](/platform/observability): Logs, metrics, and retention. --- # Databases Managed PostgreSQL and MySQL. A database is created, then provisioned; shared tiers are ready in seconds, dedicated ones take 10-20 minutes. With an [API key](/reference/api/authentication) the organisation is inferred. Session tokens must add `targetOrganisationId` to every body. ## List databases ```endpoint POST /api/databases > Databases in the organisation, newest first, paginated. permission: read:projects param page | number | 1 | 1-based page number param limit | number | 10 | Between 1 and 100 param filter | string | optional | Free text, matched against the name param sortColumn | string | created_at | Field to order by param sortOrder | string | desc | asc or desc param targetOrganisationId | string | session only | Required when using a session token response 200 | Paginated envelope { "items": [ { "id": "db_2Vx", "name": "shop-orders", "database_type": "postgresql", "tier": "db-f1-micro", "region": "europe-west1", "storage_gb": 5, "ha_enabled": false, "status": "healthy", "created_at": "2026-08-20T10:00:00.000Z" } ], "totalItems": 1, "totalPages": 1, "currentPage": 1 } code curl curl -X POST https://api.light-cloud.com/api/databases \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"limit": 100}' code Node const res = await fetch("https://api.light-cloud.com/api/databases", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ limit: 100 }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const { items } = await res.json(); items.forEach((db) => console.log(db.name, db.tier, db.status)); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/databases", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"limit": 100}, ) res.raise_for_status() for db in res.json()["items"]: print(db["name"], db["tier"], db["status"]) ``` ## Retrieve a database ```endpoint POST /api/databases/get > One database and its configuration. Never includes credentials — use the connection string endpoint for those. permission: read:projects param databaseId | string | required | The database to read param targetOrganisationId | string | session only | Required when using a session token response 200 | A database { "id": "db_2Vx", "name": "shop-orders", "database_type": "postgresql", "tier": "db-f1-micro", "region": "europe-west1", "storage_gb": 5, "ha_enabled": false, "public_ip_enabled": true, "status": "healthy", "created_at": "2026-08-20T10:00:00.000Z" } code curl curl -X POST https://api.light-cloud.com/api/databases/get \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx"}' code Node const res = await fetch("https://api.light-cloud.com/api/databases/get", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ databaseId: "db_2Vx" }), }); const database = await res.json(); ``` ## Retrieve status ```endpoint POST /api/databases/status > Poll this while a dedicated instance provisions — it can take 10-20 minutes. permission: read:projects param databaseId | string | required | The database to poll param targetOrganisationId | string | session only | Required when using a session token response 200 | Current status { "id": "db_2Vx", "status": "provisioning" } code curl until [ "$(curl -sS -X POST https://api.light-cloud.com/api/databases/status \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"databaseId\":\"$DB_ID\"}" | jq -r .status)" = "healthy" ]; do sleep 15 done ``` ## Create a database ```endpoint POST /api/databases/create > Records the database and begins provisioning. Shared tiers are ready in seconds, dedicated ones take 10-20 minutes. permission: create:projects param name | string | required | Display name param databaseType | string | platform default | `postgresql` or `mysql` param tier | string | platform default | Cloud SQL machine type: `db-f1-micro` or `db-g1-small` (shared-core, dev/test, no SLA), or `db-custom--` (dedicated) param region | string | platform default | See Limits param storageGb | number | platform default | Grows without downtime, never shrinks param haEnabled | boolean | false | High availability. Doubles the compute price param publicIpEnabled | boolean | true | Whether the instance gets a public address param authorizedNetworks | string[] | optional | CIDR list. An empty list means world-reachable param databaseName | string | generated | Initial database name param adminUser | string | generated | Admin username param adminPassword | string | generated | 12+ characters, no spaces, quotes or backslashes param projectId | string | optional | Folder to file it under response 201 | The created database { "id": "db_2Vx", "name": "shop-orders", "database_type": "postgresql", "tier": "db-f1-micro", "status": "provisioning" } code curl curl -X POST https://api.light-cloud.com/api/databases/create \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "shop-orders", "databaseType": "postgresql", "tier": "db-f1-micro", "storageGb": 5, "authorizedNetworks": ["203.0.113.10/32"] }' code Node const API = "https://api.light-cloud.com"; const headers = { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }; const post = async (path, body) => { const res = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const db = await post("/api/databases/create", { name: "shop-orders", databaseType: "postgresql", tier: "db-f1-micro", storageGb: 5, authorizedNetworks: ["203.0.113.10/32"], }); let status = db.status; while (status !== "healthy") { await new Promise((resolve) => setTimeout(resolve, 15_000)); ({ status } = await post("/api/databases/status", { databaseId: db.id })); } code Python import os import time import requests API = "https://api.light-cloud.com" headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} def post(path, **body): res = requests.post(API + path, headers=headers, json=body) res.raise_for_status() return res.json() db = post( "/api/databases/create", name="shop-orders", databaseType="postgresql", tier="db-f1-micro", storageGb=5, authorizedNetworks=["203.0.113.10/32"], ) while post("/api/databases/status", databaseId=db["id"])["status"] != "healthy": time.sleep(15) ``` > [!WARNING] > `publicIpEnabled` defaults to `true` because that is what actually gets provisioned today. Restrict reachability with `authorizedNetworks`, and treat an unset list as world-reachable. ## Get a connection string ```endpoint POST /api/databases/connection-string > Returns the full connection URL, credentials included. permission: read:projects param databaseId | string | required | The database to connect to param targetOrganisationId | string | session only | Required when using a session token response 200 | Connection URL { "connectionString": "postgresql://app:s3cr3t@34.0.0.1:5432/shop?sslmode=require" } code curl curl -X POST https://api.light-cloud.com/api/databases/connection-string \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx"}' code Node // Pipe it straight into an environment without it touching disk or a log const API = "https://api.light-cloud.com"; const headers = { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }; const post = async (path, body) => { const res = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const { connectionString } = await post("/api/databases/connection-string", { databaseId, }); const environment = await post("/api/environments/get", { environmentId }); await post("/api/environments/update", { environmentId, environmentVars: { ...environment.environment_vars, DATABASE_URL: connectionString, }, }); code Python import os import requests API = "https://api.light-cloud.com" headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} def post(path, **body): res = requests.post(API + path, headers=headers, json=body) res.raise_for_status() return res.json() connection = post("/api/databases/connection-string", databaseId=database_id) environment = post("/api/environments/get", environmentId=environment_id) post( "/api/environments/update", environmentId=environment_id, environmentVars={ **environment["environment_vars"], "DATABASE_URL": connection["connectionString"], }, ) ``` > [!IMPORTANT] > This response contains the password in clear text. Do not log it, and do not write it to a file your CI archives. See [Connecting](/platform/databases/connect). ## Update a database ```endpoint POST /api/databases/update > Changes size or configuration. Storage grows without downtime and never shrinks; changing tier restarts the instance. permission: update:projects param databaseId | string | required | The database to update param name | string | optional | Display name param tier | string | optional | Restarts the instance param region | string | optional | See Limits param storageGb | number | optional | Can only increase param haEnabled | boolean | optional | Doubles the compute price response 200 | The updated database { "id": "db_2Vx", "tier": "db-custom-1-3840", "storage_gb": 20 } code curl curl -X POST https://api.light-cloud.com/api/databases/update \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx", "tier": "db-custom-1-3840", "storageGb": 20}' ``` ## Rotate the password ```endpoint POST /api/databases/rotate-password > Issues new credentials. Applications holding the old ones break immediately. permission: update:projects param databaseId | string | required | The database to rotate param newPassword | string | generated | 12+ characters, no spaces, quotes or backslashes param targetOrganisationId | string | session only | Required when using a session token response 200 | The new connection details { "connectionString": "postgresql://app:n3wp4ss@34.0.0.1:5432/shop?sslmode=require" } code curl curl -X POST https://api.light-cloud.com/api/databases/rotate-password \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx"}' ``` > [!WARNING] > Update the [variables](/platform/environment-variables) of anything using this database and redeploy, or it stays broken. ## Read metrics ```endpoint POST /api/databases/metrics > CPU, memory, storage and connection counts over a window. permission: read:projects param databaseId | string | required | The database to measure param timeRange | string | 1h | `1h`, `6h`, `24h` or `7d` param targetOrganisationId | string | session only | Required when using a session token response 200 | Series per metric { "databaseId": "db_2Vx", "metrics": { "cpu": [{ "t": "2026-08-28T08:00:00.000Z", "v": 0.12 }], "connections": [{ "t": "2026-08-28T08:00:00.000Z", "v": 7 }] } } code curl curl -X POST https://api.light-cloud.com/api/databases/metrics \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx", "timeRange": "24h"}' ``` ## Delete a database ```endpoint POST /api/databases/delete > Removes the instance and its data. Not reversible. permission: delete:projects param databaseId | string | required | The database to delete param targetOrganisationId | string | session only | Required when using a session token response 200 | Deletion accepted { "message": "Database deleted" } code curl curl -X POST https://api.light-cloud.com/api/databases/delete \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"databaseId": "db_2Vx"}' ``` ## Other endpoints | Endpoint | Body | Description | | --- | --- | --- | | `POST /api/databases/provision` | `databaseId` | Starts provisioning for a database created but not yet built | | `POST /api/databases/dump` | `databaseId` | Produces a `pg_dump` or `mysqldump` file to download | | `POST /api/databases/import` | `databaseId` | The other direction | | `POST /api/databases/move` | `databaseId`, `targetFolderId` | Files it under a different folder | | `GET /api/databases/:targetOrganisationId/:databaseId/stream` | | Server-sent events for provisioning progress | ### Data explorer Reading and editing rows directly, the same surface the console's Data tab uses. Each takes `databaseId`. | Endpoint | Description | | --- | --- | | `POST /api/databases/explorer/schema` | Tables and columns | | `POST /api/databases/explorer/rows` | Paged rows of one table | | `POST /api/databases/explorer/query` | Run a statement | | `POST /api/databases/explorer/row` | Insert, update, or delete one row | | `POST /api/databases/explorer/table` | Table-level operations | > [!NOTE] > The explorer and dump endpoints sit behind feature flags. If they answer with a "not enabled" message on your account, check `GET /api/config/feature-flags`. ## Related - [Databases](/platform/databases): Engines, tiers, and how provisioning works. - [Connecting](/platform/databases/connect): Connection strings and clients. - [Limits](/reference/limits#databases): Tiers, storage, and password rules. --- # Uploads Deploying without a repository takes three calls and one `PUT`. The archive goes straight to storage over a signed URL, so it never passes through the API. 1. `POST /api/upload/request-url` — get a signed URL and an `uploadId` 2. `PUT` the zip to that URL 3. `POST /api/upload/complete` — tell the platform the bytes landed 4. [`POST /api/applications/create-from-upload`](/reference/api/applications#create-from-an-upload) — build and deploy it > [!TIP] > Zip the contents of the project directory, not the directory itself. A single top-level folder inside the archive puts every path one level deeper than the build expects. ## Request an upload URL ```endpoint POST /api/upload/request-url > Reserves an upload and returns a signed URL valid for 15 minutes. `maxSize` is 100 MB by default; passing a larger `fileSize` is rejected before you waste the transfer. permission: create:projects param fileName | string | source.zip | Name recorded for the archive param contentType | string | application/zip | Must match the Content-Type you PUT with param fileSize | number | optional | Bytes. Checked against maxSize up front param targetOrganisationId | string | session only | Required when using a session token response 200 | Signed URL and upload id { "uploadId": "upl_8Xa1", "signedUrl": "https://storage.googleapis.com/lc-uploads/...", "gcsPath": "uploads/org_1Ab/upl_8Xa1/source.zip", "expiresAt": "2026-08-28T07:15:00.000Z", "maxSize": 104857600 } code curl curl -X POST https://api.light-cloud.com/api/upload/request-url \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"fileSize\": $(wc -c < source.zip)}" code Node import { stat } from "node:fs/promises"; const { size } = await stat("source.zip"); const res = await fetch("https://api.light-cloud.com/api/upload/request-url", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ fileSize: size }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const { uploadId, signedUrl } = await res.json(); code Python import os import requests res = requests.post( "https://api.light-cloud.com/api/upload/request-url", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"fileSize": os.path.getsize("source.zip")}, ) res.raise_for_status() upload = res.json() upload_id, signed_url = upload["uploadId"], upload["signedUrl"] ``` ## Upload the archive The `PUT` goes to the signed URL, not to the API. **No `Authorization` header** — the signature is the authorisation. The `Content-Type` must match what you asked for, or the signature will not verify. ```codetabs ### curl curl -X PUT "$SIGNED_URL" \ -H "Content-Type: application/zip" \ --upload-file source.zip ### Node import { readFile } from "node:fs/promises"; const upload = await fetch(signedUrl, { method: "PUT", headers: { "Content-Type": "application/zip" }, body: await readFile("source.zip"), }); if (!upload.ok) throw new Error(`Upload failed: ${upload.status}`); ### Python import requests with open("source.zip", "rb") as handle: res = requests.put( signed_url, data=handle, headers={"Content-Type": "application/zip"}, ) res.raise_for_status() ### Java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; var put = HttpRequest.newBuilder() .uri(URI.create(signedUrl)) .header("Content-Type", "application/zip") .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("source.zip"))) .build(); HttpClient.newHttpClient().send(put, HttpResponse.BodyHandlers.discarding()); ``` ## Complete the upload ```endpoint POST /api/upload/complete > Confirms the bytes landed and records what the archive is. The detection fields are hints carried through to the create call — leave them out and the platform inspects the archive itself. permission: create:projects param uploadId | string | required | From request-url param detectedFramework | string | optional | What you believe this is param detectedRuntime | string | optional | Container only param detectedDeploymentType | string | optional | `static` or `container` param detectedBuildCommand | string | optional | Command that produces the build param detectedOutputDirectory | string | optional | Static only param targetOrganisationId | string | session only | Required when using a session token response 200 | Upload ready to build from { "uploadId": "upl_8Xa1", "status": "ready", "size": 4823910 } code curl curl -X POST https://api.light-cloud.com/api/upload/complete \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"uploadId": "upl_8Xa1", "detectedDeploymentType": "static"}' code Node const res = await fetch("https://api.light-cloud.com/api/upload/complete", { method: "POST", headers: { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ uploadId, detectedDeploymentType: "static" }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); code Python import os import requests requests.post( "https://api.light-cloud.com/api/upload/complete", headers={"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"}, json={"uploadId": upload_id, "detectedDeploymentType": "static"}, ).raise_for_status() ``` ## Clean up abandoned uploads ```endpoint POST /api/upload/cleanup > Discards uploads that were reserved but never completed. permission: create:projects param targetOrganisationId | string | session only | Required when using a session token response 200 | How many were discarded { "removed": 3 } code curl curl -X POST https://api.light-cloud.com/api/upload/cleanup \ -H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## The whole flow ```codetabs ### curl #!/usr/bin/env bash set -euo pipefail API=https://api.light-cloud.com AUTH=(-H "Authorization: Bearer $LIGHT_CLOUD_API_KEY" -H "Content-Type: application/json") (cd dist && zip -qr ../source.zip .) upload=$(curl -sS -X POST "$API/api/upload/request-url" "${AUTH[@]}" \ -d "{\"fileSize\":$(wc -c < source.zip)}") UPLOAD_ID=$(echo "$upload" | jq -r .uploadId) SIGNED_URL=$(echo "$upload" | jq -r .signedUrl) curl -sS -X PUT "$SIGNED_URL" \ -H "Content-Type: application/zip" \ --upload-file source.zip curl -sS -o /dev/null -X POST "$API/api/upload/complete" "${AUTH[@]}" \ -d "{\"uploadId\":\"$UPLOAD_ID\",\"detectedDeploymentType\":\"static\"}" curl -sS -X POST "$API/api/applications/create-from-upload" "${AUTH[@]}" \ -d "{ \"name\": \"marketing-site\", \"uploadId\": \"$UPLOAD_ID\", \"deploymentType\": \"static\", \"outputDirectory\": \".\" }" ### Node import { readFile, stat } from "node:fs/promises"; const API = "https://api.light-cloud.com"; const headers = { Authorization: `Bearer ${process.env.LIGHT_CLOUD_API_KEY}`, "Content-Type": "application/json", }; const post = async (path, body) => { const res = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); }; const { size } = await stat("source.zip"); // 1. Signed URL const { uploadId, signedUrl } = await post("/api/upload/request-url", { fileSize: size, }); // 2. Straight to storage — no Authorization header here const put = await fetch(signedUrl, { method: "PUT", headers: { "Content-Type": "application/zip" }, body: await readFile("source.zip"), }); if (!put.ok) throw new Error(`Upload failed: ${put.status}`); // 3. Confirm await post("/api/upload/complete", { uploadId, detectedDeploymentType: "static", }); // 4. Build and deploy const app = await post("/api/applications/create-from-upload", { name: "marketing-site", uploadId, deploymentType: "static", outputDirectory: ".", }); console.log(app.id); ### Python import os import requests API = "https://api.light-cloud.com" headers = {"Authorization": f"Bearer {os.environ['LIGHT_CLOUD_API_KEY']}"} def post(path, **body): res = requests.post(API + path, headers=headers, json=body) res.raise_for_status() return res.json() # 1. Signed URL upload = post("/api/upload/request-url", fileSize=os.path.getsize("source.zip")) # 2. Straight to storage — no auth header with open("source.zip", "rb") as handle: requests.put( upload["signedUrl"], data=handle, headers={"Content-Type": "application/zip"}, ).raise_for_status() # 3. Confirm post( "/api/upload/complete", uploadId=upload["uploadId"], detectedDeploymentType="static", ) # 4. Build and deploy app = post( "/api/applications/create-from-upload", name="marketing-site", uploadId=upload["uploadId"], deploymentType="static", outputDirectory=".", ) print(app["id"]) ``` ## Related - [Applications](/reference/api/applications): Creating an application from the upload. - [Deploy from an upload](/create/upload): The same flow in the console. - [Frameworks](/platform/frameworks): What detection recognises. --- # GitHub Private repositories are reached through a GitHub App installation, linked to a Light Cloud organisation. One GitHub account can be linked to several organisations, and one organisation can hold several installations. These endpoints are mostly `GET` and take their parameters in the path or query string, unlike the rest of the API. ## Check repository access ```endpoint GET /api/github-app/installation-status > Whether a specific repository is reachable. `repoAccess: false` with `installed: true` means the App is on the account but was not granted this repository. permission: read:projects param organisationId | string | required | Query parameter param owner | string | required | Query parameter — repository owner param repo | string | required | Query parameter — repository name response 200 | Installation and access state { "configured": true, "installed": true, "installationId": 12345678, "accountLogin": "acme", "repoAccess": true } code curl curl -G https://api.light-cloud.com/api/github-app/installation-status \ -H "Authorization: Bearer $SESSION_TOKEN" \ --data-urlencode "organisationId=org_1Ab" \ --data-urlencode "owner=acme" \ --data-urlencode "repo=shop" code Node const params = new URLSearchParams({ organisationId: "org_1Ab", owner: "acme", repo: "shop", }); const res = await fetch( `https://api.light-cloud.com/api/github-app/installation-status?${params}`, { headers: { Authorization: `Bearer ${sessionToken}` } } ); const status = await res.json(); if (!status.repoAccess) { console.log("Grant this repository to the Light Cloud App on GitHub"); } code Python import requests res = requests.get( "https://api.light-cloud.com/api/github-app/installation-status", headers={"Authorization": f"Bearer {session_token}"}, params={"organisationId": "org_1Ab", "owner": "acme", "repo": "shop"}, ) res.raise_for_status() status = res.json() if not status["repoAccess"]: print("Grant this repository to the Light Cloud App on GitHub") ``` ## Start an installation ```endpoint GET /api/github-app/install > Returns the GitHub URL to send a person to. GitHub returns them to /api/github-app/callback, which links the installation. permission: read:projects response 200 | Where to send the user { "url": "https://github.com/apps/light-cloud/installations/new?state=..." } code curl curl https://api.light-cloud.com/api/github-app/install \ -H "Authorization: Bearer $SESSION_TOKEN" code Node const res = await fetch("https://api.light-cloud.com/api/github-app/install", { headers: { Authorization: `Bearer ${sessionToken}` }, }); const { url } = await res.json(); // Open `url` in the browser; GitHub redirects back when the App is installed ``` ## List repositories ```endpoint GET /api/github-app/organisation/:orgId/repositories > Every repository the organisation's linked installations can reach. permission: read:projects param orgId | string | required | Path parameter — the organisation response 200 | Repositories [ { "id": 45678901, "name": "shop", "full_name": "acme/shop", "private": true, "default_branch": "main" } ] code curl curl https://api.light-cloud.com/api/github-app/organisation/org_1Ab/repositories \ -H "Authorization: Bearer $SESSION_TOKEN" code Node const res = await fetch( `https://api.light-cloud.com/api/github-app/organisation/${organisationId}/repositories`, { headers: { Authorization: `Bearer ${sessionToken}` } } ); const repositories = await res.json(); repositories.forEach((repo) => console.log(repo.full_name, repo.default_branch)); code Python import requests res = requests.get( f"https://api.light-cloud.com/api/github-app/organisation/{organisation_id}/repositories", headers={"Authorization": f"Bearer {session_token}"}, ) res.raise_for_status() for repo in res.json(): print(repo["full_name"], repo["default_branch"]) ``` ## List branches ```endpoint GET /api/github-app/organisation/:orgId/repositories/:owner/:repo/branches > Branches of one repository. For a public repository you do not need an installation — use POST /api/applications/public-branches instead. permission: read:projects param orgId | string | required | Path parameter — the organisation param owner | string | required | Path parameter — repository owner param repo | string | required | Path parameter — repository name response 200 | Branches [ { "name": "main", "commit_sha": "9f3c1ab" }, { "name": "develop", "commit_sha": "2b8e004" } ] code curl curl https://api.light-cloud.com/api/github-app/organisation/org_1Ab/repositories/acme/shop/branches \ -H "Authorization: Bearer $SESSION_TOKEN" code Node const res = await fetch( `https://api.light-cloud.com/api/github-app/organisation/${organisationId}` + `/repositories/acme/shop/branches`, { headers: { Authorization: `Bearer ${sessionToken}` } } ); const branches = await res.json(); ``` ## Check several accounts ```endpoint POST /api/github-app/organisation/:orgId/check-accounts > Which of these GitHub accounts have the App installed, and which are linked to this workspace. `installed: true, linkedToThisOrg: false` is the case that trips people up — the App is on that account, but this workspace cannot use it until it is linked. permission: read:projects param orgId | string | required | Path parameter — the organisation param accountLogins | string[] | required | GitHub account logins to check response 200 | Per-account state { "accounts": { "acme": { "installed": true, "linkedToThisOrg": true }, "someone-else": { "installed": true, "linkedToThisOrg": false } } } code curl curl -X POST https://api.light-cloud.com/api/github-app/organisation/org_1Ab/check-accounts \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{"accountLogins": ["acme", "someone-else"]}' code Node const res = await fetch( `https://api.light-cloud.com/api/github-app/organisation/${organisationId}/check-accounts`, { method: "POST", headers: { Authorization: `Bearer ${sessionToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ accountLogins: ["acme", "someone-else"] }), } ); const { accounts } = await res.json(); ``` ## Managing installations | Endpoint | Description | | --- | --- | | `GET /api/github-app/installations` | Every installation visible to the user | | `GET /api/github-app/organisation/:orgId/installations` | Those linked to this organisation | | `POST /api/github-app/organisation/:orgId/link-installation` | Attaches an existing installation to this organisation | | `POST /api/github-app/organisation/:orgId/sync-installations` | Re-reads GitHub and prunes rows whose installation is gone | | `DELETE /api/github-app/organisation/:orgId/installations/:installId` | Unlinks it from this organisation | | `DELETE /api/github-app/organisation/:orgId/installations/:installId/uninstall` | Unlinks **and** removes the App from the GitHub account | | `GET /api/github-app/organisation/:organisationId/stream` | Server-sent events, so a page can react as an installation completes | ## GitLab and Bitbucket The same shape exists under `/api/gitlab` and `/api/bitbucket` (connections instead of installations: `GET /organisation/:orgId/connections`, `POST /organisation/:orgId/link-connection`, `DELETE /organisation/:orgId/connections/:id`, `POST /disconnect`, and the same repository and branch listings), each behind a feature flag. Check `GET /api/config/feature-flags` before relying on them. ## Related - [Applications](/reference/api/applications): Creating an application from a repository. - [Git automation](/create/git-automation): Deploy on push, branch rules, and stale environments. - [Deployments](/platform/deployments): What happens after a push. --- # Errors A failed call returns a non-2xx status and a JSON body with a `message`: ```json { "message": "Organisation ID is required." } ``` Errors that map to a known condition also carry a `code` and sometimes an `action`: ```json { "code": "CB001", "message": "Payment method required", "action": "Add a payment method in Billing settings." } ``` `message` is written for a person to read. `code` is the one to branch on - messages get reworded, codes do not. ## Status codes | Status | Means | Do | | --- | --- | --- | | `400` | The body was wrong - a missing id, a bad value | Fix the request; retrying will not help | | `401` | No token, or an expired one | Refresh the [session token](/reference/api/authentication#session-tokens) and retry once; an API key that returns this is revoked or expired | | `402` | Billing is blocking the action, or the plan does not include API keys | See [Billing](/billing/billing) and [Authentication](/reference/api/authentication#plans) | | `403` | Authenticated, but not allowed | Check the role and the [permission](#permissions) below | | `404` | No such resource, or not yours | Do not distinguish these - the API deliberately does not | | `409` | Conflicts with what already exists | Usually a duplicate name | | `429` | Rate limited | Honour `Retry-After` when present | | `5xx` | The platform, not you | Retry with backoff; if it persists, [contact us](https://www.light-cloud.com/contact) | The default for a rejected request is `400`, not `422`. ## Error codes The first letter is the service (`C` for console), the second the category, then a number. ### Deploy | Code | Status | Message | | --- | --- | --- | | `CD001` | 500 | Container deployment failed | | `CD002` | 400 | Container deployment failed due to invalid configuration | | `CD003` | 500 | Container build failed | | `CD004` | 500 | Failed to push container image | | `CD005` | 500 | Failed to deploy container to cloud | | `CD006` | 504 | Deployment timed out | | `CD007` | 500 | Failed to delete container deployment | | `CD010` | 500 | Static site deployment failed | | `CD011` | 500 | Static site build failed | | `CD012` | 500 | Failed to upload static files | `CD003` and `CD011` are your build, not the platform - read the [deployment logs](/reference/api/deployments#retrieve-a-deployment). ### Auth | Code | Status | Message | | --- | --- | --- | | `CA001` | 401 | Authentication failed | | `CA002` | 403 | You do not have permission to perform this action | | `CA003` | 403 | You do not have access to this organisation | `CA001` with an API key means the key is wrong, revoked, or expired - the API does not distinguish between them. `CA003` usually means the wrong `targetOrganisationId`, not a missing role. With a key it means the body named an organisation the key does not belong to; omit the field and it is filled in correctly. ### Billing | Code | Status | Message | | --- | --- | --- | | `CB001` | 402 | Payment method required | | `CB003` | 402 | Included usage used up | `CB003` means a free workspace has used its $1 of included usage for the cycle: server apps and deploys are paused until the next cycle or an upgrade, while static sites keep serving. Upgrading resumes them within minutes; see [Billing](/billing/billing#the-free-plan). A `402` on **every** request made with an API key, rather than on one particular action, means the organisation's plan no longer includes keys - the entitlement is re-checked on each call, so a downgrade stops existing keys working. Move to a paid plan, or use the [MCP server](/deploy-with-ai/mcp-server), which works on every account. ### General | Code | Status | Message | | --- | --- | --- | | `CG001` | 500 | Something went wrong | | `CG002` | 400 | Invalid request | | `CG003` | 404 | Resource not found | | `CG004` | 409 | This action conflicts with existing data | | `CG005` | 429 | Too many requests | | `CG006` | 503 | Service temporarily unavailable | ### Platform services | Code | Status | Message | | --- | --- | --- | | `CR001` | 503 | Deployment service is temporarily unavailable | | `CR002` | 502 | Deployment service returned an error | | `CR003` | 504 | Deployment service timed out | | `CS001` | 503 | Database connection failed | | `CS002` | 500 | Database operation failed | | `CX001` | 502 | Failed to connect to GitHub | | `CX002` | 502 | Payment service error | All of these are worth retrying with backoff. `CR003` in particular does not mean the deploy failed - it means the answer was slow. Poll [environment status](/reference/api/environments#retrieve-status) before retrying the deploy, or you will start a second one. ## Permissions `403` with `CA002` means your role in that organisation lacks the permission the endpoint checks: | Permission | Endpoints | | --- | --- | | `read:projects` | Every list, get, status, logs, metrics, and activity call | | `create:projects` | Creating applications, environments, and databases | | `update:projects` | Update, scale, deploy, rollback, rename, move, domains, password rotation | | `delete:projects` | Deleting applications, environments, and databases | | `read:users` | Listing organisation members | | `read:billing` | Invoices, usage, and payment methods | | `update:all` | Organisation-level settings | Read your role from [`/api/auth/profile`](/reference/api/authentication#retrieve-the-current-user). An API key holds one of two access levels rather than a role: read-only maps to `read:projects`, deploy-and-manage adds create, update, and delete. No key holds billing or organisation permissions, so those endpoints answer `403` to every key. ## Retrying safely Creates are not idempotent - there is no idempotency key yet, so a retried `create` after a timeout can leave you with two applications. On a timeout, list first and check before retrying. Deletes and updates are safe to repeat. ## Related - [API overview](/reference/api): Conventions and rate limits. - [Authentication](/reference/api/authentication): Tokens and refresh. - [Contact](https://www.light-cloud.com/contact): When it is the platform, not you. --- # Limits The numbers behind the platform, collected. Each links to the page that explains it. ## Container sizes See [Application settings](/create/application-settings#size) and [Pricing](/billing/pricing#containers). | Size | vCPU | Memory | Max concurrency | Notes | | --- | --- | --- | --- | --- | | Nano | 1 | 256 MB | 40 | Scale to zero only | | Micro | 1 | 512 MB | 80 | Default size | | Small | 1 | 1 GB | 100 | | | Medium | 2 | 2 GB | 200 | | | Large | 4 | 4 GB | 300 | | Sizes up to 8 vCPUs and 32 GB are coming. ## Scaling See [Scaling](/create/scaling). | Setting | Range | Default | | --- | --- | --- | | Min instances | 0-5 | 0 (scale to zero) | | Max instances | 1-100, up to the plan limit | 10 | | Concurrency | 1 to the size's cap | 80 | | CPU target | percentage | 80% | | SSR frontends | pinned Micro, scale 0-3 | [policy](/create/scaling#the-ssr-policy) | ## Regions See [Application settings](/create/application-settings#region) and [Pricing](/billing/pricing#regions). | Status | Regions | | --- | --- | | Available (tier 1) | Belgium `europe-west1`, Iowa `us-central1`, South Carolina `us-east1`, Oregon `us-west1` | | Coming (tier 1) | Netherlands `europe-west4`, Taiwan `asia-east1` | | Coming (tier 2, +20%) | London, Frankfurt, Tokyo, Singapore, Sydney | Shared-tier databases run in Belgium (`europe-west1`) today. ## Databases See [Databases](/platform/databases) and [Database settings](/create/database-settings). | Item | Value | | --- | --- | | Engines | PostgreSQL 15 (port 5432), MySQL 8.0 (port 3306) | | Tiers | Dev (shared), Starter, Pro, Business, Enterprise, EnterprisePlus | | Shared storage | 1, 2, 5, or 10 GB | | Dedicated storage | 10, 20, 50, or 100 GB (250 and 500 GB coming) | | Storage changes | Grow without downtime; never shrink | | Provisioning time | Shared: seconds; dedicated: 10-20 minutes | | Password rules | 12+ characters; no spaces, quotes, or backslashes | | High availability | 2x compute price | ## Ports See [Build settings](/create/build-settings#port). `PORT` is set in every container (default `8080`). | Runtime | Default port | | --- | --- | | Node.js, Go, Java, PHP, .NET, Dockerfile | 8080 | | Python | 8000 | | Ruby | 3000 | | Next.js, Nuxt, SvelteKit, Remix | 3000 | | AdonisJS | 3333 | ## Retention See [Observability](/platform/observability). | Data | Kept | | --- | --- | | Deploy history | Last 20 deploys per environment | | Runtime logs | 7 days, searchable | | Metrics | 90 days, 30-second refresh while open | | Activity history | Settings and variable changes, without values | ## Plans See [Pricing](/billing/pricing) and [Billing](/billing/billing). Plan limits are what a plan lets you create; they are separate from the usage it includes. When something is not in your plan, the console says so and names the plan that includes it. Free is the plan formerly called Hobby (its id is still `hobby`). | | Free | Lite | Starter | Pro | Business | | --- | --- | --- | --- | --- | --- | | Price | $0 | $5/month | $19/month | $49/month | $149/month | | Paid yearly | - | $50/year | $190/year | $490/year | $1,490/year | | Usage included | $1/month | $5/month | $19/month | $49/month | $149/month | | Static sites | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited | | Static bandwidth, fair use per workspace | 20 GB/month | 1 TB/month | 1 TB/month | 1 TB/month | 1 TB/month | | Preview URLs | One per branch, unlimited | One per branch, unlimited | One per branch, unlimited | One per branch, unlimited | One per branch, unlimited | | Server apps, any kind | 3 | 5 | 10 | 30 | 100 | | Container sizes | Nano, Micro | Nano, Micro | Nano to Small | Nano to Medium | Nano to Large | | Databases | None | 1, Dev (shared) tier | Unlimited, tiers Dev, Starter, Pro | Unlimited, tiers Dev to Business | Unlimited, tiers Dev to Business | | Custom domains | No, apps use their light-cloud.io address | Yes | Yes | Yes | Yes | | "by Light Cloud" link on pages | Yes, bottom-right of every HTML page | No | No | No | No | | Always-on instances | No | No | Yes | Yes | Yes | | Instances per app | Up to 3 | Up to 3 | Up to 10 | Up to 25 | Up to 50 | | Team members | 1 | 1 | 3 | Unlimited | Unlimited | | Workspaces | One free workspace per person | Unlimited, each on its own plan | Unlimited, each on its own plan | Unlimited, each on its own plan | Unlimited, each on its own plan | | When the included usage is used up | Server apps and deploys pause until the next cycle or an upgrade; static sites keep serving; never billed | Extra usage on the next invoice; optional usage limit | Extra usage on the next invoice; optional usage limit | Extra usage on the next invoice; optional usage limit | Extra usage on the next invoice; optional usage limit | Server apps are anything that runs as a container: APIs, server-rendered frontends, Dockerfiles. Static sites and branch previews do not count against the limit. More than 100 server apps: [talk to us](https://www.light-cloud.com/contact?subject=sales). Each person gets one free workspace. A second or later workspace can be created, but it cannot create apps or deploy until it is on a paid plan. Free workspaces that existed before this rule keep working. ## Domains and TLS See [Custom domains](/platform/custom-domains). | Item | Value | | --- | --- | | Plans | Every paid plan, from Lite. Free apps use their light-cloud.io address; a domain already on a Free workspace goes on hold 14 days after the owner is emailed, until the workspace moves to a paid plan | | Domains per environment | One target per domain, platform-wide | | Certificates | Issued and renewed automatically | | TLS | 1.2 minimum, everywhere | | DNS propagation | Usually minutes, up to 48 hours | --- # Glossary Platform terms, in alphabetical order, each linking to its full page. ## Application One deployed project, linked to one repository (or created from an [upload](/create/upload) or [template](/create/templates)). Holds the settings its environments inherit. See [Deployments](/platform/deployments). ## Build The step that turns a commit into something runnable: a folder of static files or a container image. Runs on Cloud Build. See [Builds](/platform/deployments/builds). ## Cold start The seconds an app at zero instances takes to boot for its first request after idle. The price of [scale to zero](#scale-to-zero). See [Scaling](/create/scaling#cold-starts). ## Concurrency How many requests one instance serves at the same time. Default 80, capped per [size](#size). See [Scaling](/create/scaling#concurrency). ## CPU target The average CPU percentage at which autoscaling adds an instance. Default 80%. See [Scaling](/create/scaling#cpu-target). ## Deployment One commit, built and released to one environment. Each environment keeps its last 20. See [Deploy history](/platform/deployments#deploy-history). ## Detection Light Cloud reading a repository to work out the framework, package manager, build command, and output directory. See [How detection works](/platform/frameworks#how-detection-works). ## Environment One running copy of an application, with its own URL, settings, and variables. One per connected branch: one production, the rest [previews](#preview-environment). See [How it works](/getting-started/how-it-works#applications-and-environments). ## Environment variable A configuration value or secret set on an application or environment, delivered at build time (static) or runtime (container). See [Environment variables](/platform/environment-variables). ## Extra usage Usage above what the plan includes. On a paid plan it is added to the next invoice at the same rates; nothing pauses unless you set a [usage limit](#usage-limit). Free has no extra usage: its server apps [pause](#paused) instead, and it is never billed. See [Billing](/billing/billing#extra-usage). ## Fair use The static-site bandwidth a workspace's sites share each month: 20 GB on Free, 1 TB on paid plans. It is not a meter and is never charged; going past it means an email first. See [Pricing](/billing/pricing#static-sites). ## Included usage The amount of usage a plan includes each month, worth the plan's price ($5 on Lite, $19 on Starter, $49 on Pro, $149 on Business); Free includes $1. Everything you run counts against it at the same rates on every plan. See [Billing](/billing/billing). ## Instance One running copy of a container app. Autoscaling adds and removes instances between the [min](/create/scaling#min-instances) and [max](/create/scaling#max-instances). Billed per instance-hour. ## Output directory The folder a static build writes the finished site into - exactly what the CDN serves. See [Build settings](/create/build-settings#output-directory). ## Paused What happens on Free when its $1 of included usage is used up: server apps and deploys pause until the next billing cycle or an upgrade, while static sites keep serving. Nothing is billed. See [Billing](/billing/billing#the-free-plan). ## Plan A fixed price, monthly or yearly, that includes the same amount of usage each month - Free, Lite, Starter, Pro or Business. See [Pricing](/billing/pricing). ## Plan limits What a plan lets you create: how many server apps, which sizes and database tiers, how many databases, custom domains, always-on instances, team members. Static sites and preview URLs are unlimited on every plan. Separate from the usage it includes. See [Limits](/reference/limits#plans). ## Point-in-time recovery Restoring a PostgreSQL database to a chosen moment rather than to the last daily backup. See [Backups](/platform/databases/backups). ## Preview environment An environment created for a branch, at its own URL, removed when the branch is deleted. See [Preview environments](/platform/preview-environments). ## Production environment The protected environment deployed from the production branch (`main` or `master` by default). Cleanup never touches it. See [Deployments](/platform/deployments#the-production-environment). ## Region The data centre a container app or database runs in. Tier-1 regions bill at base rates; tier-2 (coming) carry a surcharge. See [Application settings](/create/application-settings#region). ## Root directory The folder of a monorepo an application builds from. Pushes elsewhere in the repo don't rebuild it. See [Application settings](/create/application-settings#root-directory). ## Scale to zero An app with min instances 0 sleeping when idle - and not billing while asleep. See [Billing](/billing/billing#scale-to-zero). ## Shared tier The pooled database tier: your own credentials and database on a shared instance, ready in seconds, about $4 a month, from Lite up. See [Databases](/platform/databases#the-shared-tier). ## Size The vCPU and memory of each instance, Nano through Large. See [Application settings](/create/application-settings#size). ## Stack A template that creates an application and a connected database in one go - WordPress, Open SaaS. See [Templates](/create/templates#stacks). ## Static site A build served entirely from the CDN: no server, no region, no cold starts, bandwidth included up to [fair use](#fair-use). Unlimited on every plan. See [Frameworks](/platform/frameworks#frontend). ## Usage alerts The emails sent at 80% of the included usage and when it is used up, plus one optional alert at a dollar amount of your choosing. Alerts only email; they never pause anything. See [Billing](/billing/billing#usage-alerts). ## Usage limit An optional ceiling on a paid plan's extra usage. At an amount you choose it either alerts you or pauses server apps. Offered at checkout, off unless you choose it. See [Billing](/billing/billing#usage-limit). --- # ICE ICE is Light Cloud's open-source side. It is a visual editor for cloud infrastructure: drag blocks onto a canvas, connect them, preview the cost, and deploy to your own cloud accounts. ## ICE and the platform They solve different problems and share nothing but a maker: | | Light Cloud platform | ICE | | --- | --- | --- | | Deploys | Your app | Your infrastructure | | Runs on | Light Cloud's managed infrastructure | Your own GCP, AWS, or Azure accounts | | You manage | Nothing below the app | The cloud accounts it deploys into | | License | Hosted service | Apache 2.0, open source | If you want a URL for your app, you want [the platform](/getting-started/introduction). If you want to design and provision cloud resources you operate yourself, you want ICE. ## Where to learn more - [Light Cloud Labs](https://light-cloud.com/labs): The overview of ICE, its blocks, and templates. - [ICE on the website](https://light-cloud.com/labs/ice): What is in the editor and how it works. - [GitHub](https://github.com/light-cloud-com/ice): Source, issues, and releases. ## Provider support GCP is the mature target. AWS and Azure ship the main primitives and are experimental. A few more providers are in preview. See the [providers page](https://light-cloud.com/labs/providers). ---