# sixpanel.json — your website's setup in a config file in its repository

> Source: https://www.allsweb.com/sixpanel/docs/sixpanel-json
> Markdown for agents: https://www.allsweb.com/sixpanel/docs/sixpanel-json.md
> Publisher: AllsWeb (www.allsweb.com)

Part of: SixPanel documentation

**What this page is for:** keeping a website's whole setup — how it is built and started, the
variables it needs, its cron jobs, its PHP settings — in one file, `sixpanel.json`, at the top of
its own repository (or its ZIP). SixPanel reads that file at **every deploy** and sets the website
up the way it says, the way `vercel.json` works on Vercel. Change the file, push, and the next
deploy applies the change; nothing has to be clicked twice on a new server.

**You need**

- A website of one of these kinds: static website, Node.js app, PHP website, WordPress, Python app
  or Java app ([Websites and apps](https://www.allsweb.com/sixpanel/docs/websites-and-apps)). A Docker app, a reverse proxy and a
  Create with AI website never deploy files of their own, so they have no such file; a 6amMart
  project's setup stays on its own pages (a `sixpanel.json` in a shop's code is reported in the
  update log and nothing of it is applied).
- Nothing to install. A website with no `sixpanel.json` deploys exactly as it always did.

---

## The file

`sixpanel.json` sits at the **top** of the website's files: the root of its git repository, or the
top of the ZIP or folder you upload. It is plain JSON — no comments — of at most 64 KB, and it must
be a plain file (a link or a folder at that name is refused). The smallest file is:

```json
{ "version": 1, "start": "node server.js" }
```

`version` is required and is `1`. Every other key is optional: say one thing, or everything. A
`$schema` line is allowed, so your editor can check the file as you type it (see
[The JSON Schema](#the-json-schema)).

**When it is read:** at every deploy — a git push, **Deploy now**, an upload — once the new
version's files have arrived and before anything of them runs. In this order:

1. **Checked.** A file that is not valid JSON, says another `version`, or has a value of the wrong
   kind (a command of two lines, a folder outside the project, a cron schedule that is none) **stops
   the deploy before anything changes**, and the live version keeps serving. The deploy log names
   the key that is wrong. A key SixPanel does not know is only a warning (a newer file on an older
   panel), never a stop.
2. **Built with it.** The new version is built and started with what the file says.
3. **Applied when it is live — and only then.** The website's settings take the file's values, the
   environment defaults are added, and the cron jobs are made exactly the file's list. A deploy that
   fails changes **nothing** the file asked for.

Every deploy log has one **sixpanel.json** section: each change as a line ("start command: node
server.js → node .next/standalone/server.js"), each cron job added, changed or removed, each key it
ignored and why, and each variable it needs that the website does not have yet.

## What it can set

| Key | For | What it sets |
|---|---|---|
| `kind` | all | The kind of website the file is written for. A website of another kind is **never converted**: the deploy stops and says both kinds. |
| `runtime.node` | Node.js, a static website with a build | The Node.js line, like `"24"`. It must be installed on this server (**Software → Node.js**); if it is not, the deploy stops and says so. |
| `runtime.php` | PHP, WordPress | The PHP version, like `"8.4"` — composer and the deploy commands already run on it, and the website moves to it when the new version goes live. It must be installed (**Software → PHP**). |
| `runtime.python` | Python | The Python the app expects, like `"3.13"`. A Python app runs on the server's own Python, so another one stops the deploy and names both. |
| `runtime.java` | Java | The Java version, like `"21"` — that one when it is installed, otherwise the oldest installed one that is newer, or the server's own package is installed by the build. |
| `build.install` | Node.js, Python | The install command. |
| `build.command` | static, Node.js, Java | The build command. |
| `build.output` | static, Node.js, Java | The folder that is served (static) or the build's output folder (Node.js); for a Java app, the JAR its build makes, like `target/app.jar`. |
| `start` | Node.js, Python | The start command (a Python app's own start command replaces gunicorn/uvicorn). |
| `root` | PHP, static without a build | The folder the web server serves, like `public`. |
| `env.required` | Node.js, Python, Java, PHP | Names of the variables the app needs. A missing one does not stop the deploy — the log and the website's Overview say which are missing. You type their values on the website's **Environment** page. |
| `env.defaults` | Node.js, Python, Java, PHP | Values that are **not secret**, set only where the website has no value yet (a value you set on the page always wins). A PHP website gets them as lines in its `.env`. |
| `cron` | every kind but static | The website's cron jobs from the file — see [Cron jobs from the file](#cron-jobs-from-the-file). |
| `hooks.afterBuild` | kinds with a build, and PHP | A command run as the website, in the new version, after its build (a PHP website: after composer, before its deploy commands) and before it goes live. If it fails, the deploy stops and visitors keep the version they had. |
| `php.settings` | PHP, WordPress | Only what the website's **PHP** page lets you change, within its limits: `memory_limit`, `upload_max_filesize`, `post_max_size`, `max_execution_time`, `max_input_time`, `max_input_vars`, `display_errors`, `short_open_tag`, `timezone`, `allow_url_fopen`, `session_lifetime`. |
| `php.extensions` | PHP, WordPress | Extensions to switch on for this website, like `["intl", "imagick"]`. They are only ever turned on — one you switched on yourself stays on. |
| `php.mail` | PHP, WordPress | `false` turns PHP `mail()` off for the website. `true` turns it **on** only after the server's owner says yes, with the risk the PHP page's switch states (see [asked once](#domains-databases-redis-and-caches-in-sixpanel-json-asked-once)). |

A variable whose **name reads like a secret** — ending in `_SECRET`, `_KEY` or `_TOKEN`, or holding
`PASSWORD` or `_PRIVATE`, or `DATABASE_URL` — is never taken from `env.defaults`: its value would
sit in your repository for anyone who can read it. Name it in `env.required` and type its value on
the **Environment** page.

## The rest of the website: three kinds of key

The file can set everything a website's pages set. What keeps that safe is that each key is in one
of three kinds, by what it can cost or expose:

- **Applied at every deploy** — what only changes how this website builds and runs in its own
  sandbox: the table above and the first table below.
- **Asked once — a yes first** — what creates or exposes something: a domain, a database, the
  website's own Redis, a cache of its own, Apache, nginx directives, PHP `mail()` on. Nothing of it
  happens before a yes.
- **Never, whoever says yes** — the server's firewall, SSH and FTP, Cloudflare, the panel's own
  settings, another website, a secret's value, anything past your account's limits.

Every value is checked the way the page that sets it checks it, with that page's limits, and saved
by that page's own save. A value the page would refuse stops the deploy with the page's own
sentence; so does a value past your account's limits (its memory, its PHP workers, the memory one
PHP request may use) — a file never gets a website more than its package holds, and never quietly
less than it says.

### Applied at every deploy

| Key | For | What it sets |
|---|---|---|
| `node.memoryMb` | Node.js | The app's memory limit in MB (128 to 65536) — **Settings → Advanced → Memory limit**. Its heap is three quarters of it. |
| `node.pageCache` | Node.js | `true` turns on the page cache: a page the app marks `public` is kept for 10 seconds for visitors who carry no cookie. The same as Settings → Advanced → Page cache. |
| `node.processes` | Node.js | How many copies of the app serve its visitors: `1` (the default), up to `16`, or `"auto"` (one a processor core, at most 8). The same as Settings → Advanced → Processes. Keep `1` for an app that holds sessions, websockets or timers in its own memory. |
| `python.memoryMb` | Python | The app's memory limit in MB. |
| `python.workers` | Python | gunicorn or uvicorn workers, 1 to 32. |
| `python.startTimeoutSec` | Python | How long a new version may take to answer the first time, 15 to 900 seconds. |
| `java.memoryMb` | Java | The app's memory limit in MB; the heap (`-Xmx`) follows it. |
| `java.jvmOptions` | Java | JVM options on one line, without `-Xmx`. |
| `java.args` | Java | The app's arguments, on one line. |
| `java.startTimeoutSec` | Java | As for Python. |
| `health.path` | Node.js, Python, Java | The path a new version must answer before visitors are moved to it, like `/healthz`. |
| `releases.keep` | static, Node.js, Python, Java | How many versions stay on disk, the live one among them — the others are ready for a rollback (3 unless set, 1 to 10). **Settings → Advanced**. |
| `web.maxBodyMb` | static, Node.js, Python, Java | The largest upload nginx takes, in MB (1 to 4096). **Settings → Advanced**. A PHP website takes what its PHP takes: set `php.settings.upload_max_filesize`. |
| `web.spa` | static | A single-page app: a path with no file shows `index.html`. |
| `web.notFound` | static | The page shown for a path with no file, like `404.html` (while `spa` is off). |
| `web.headers` | every kind | Response headers, as **Web Rules** takes them: `[{ "name": "X-Robots-Tag", "value": "noindex" }]`, or `"remove": true` (at most 10). The file's list is the website's header list. Never `Set-Cookie`, and never a security header SixPanel sends (`Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`) — the deploy stops; Web Rules can change those on its page. |
| `web.redirects` | every kind | Redirects: `[{ "from": "/old", "to": "/new", "status": 301 }]` — `status` 301, 302, 307 or 308 (301 unless set), `match` `exact`, `prefix` or `regex`, and `keepQuery`. They replace the website's redirects (at most 25); a Rewrite you typed on Web Rules stays. |
| `php.pool` | PHP, WordPress | The PHP workers, as the PHP page sets them: `pm` (`ondemand`, `dynamic`, `static`), `max_children` (1 to 200), `idle_timeout` (5 to 600), `max_requests` (50 to 100000). |
| `php.sessions` | PHP | `files` or `redis`, as the PHP page's **Sessions** switch. Redis needs the website's own Redis (one the file asks for counts once it has its yes). A switch signs every visitor out once. |
| `cache.page` | WordPress | WordPress's page cache on or off. On turns Varnish off for the website: the two are never on together. |
| `cache.brotli` | every kind | Brotli is the whole server's (**Software → Brotli**): `true` holds when the server has it on, and is otherwise listed as the owner's; `false` is ignored — one website does not turn it off. |
| `cache.varnish.purgeOnDeploy` | websites behind Varnish | `false` keeps Varnish's copy at a deploy (it then ages out by its own time) — **Caching → Varnish → Clear it at every deploy**. Purge and a rollback always clear it. |

### Domains, databases, Redis and caches in sixpanel.json: asked once

| Key | For | What it makes |
|---|---|---|
| `domains` | every kind | `[{ "name": "shop.example.com", "primary": true, "www": true }, { "name": "old.example.com", "redirectTo": "shop.example.com" }]` — each name added as **Domain & SSL** adds it, with HTTPS the panel's own way (Cloudflare when its token is there, otherwise the usual certificate). A name that does not reach this server yet is connected by itself once it does. `redirectTo` is the primary domain. |
| `databases` | Node.js, Python, Java, PHP | `[{ "engine": "postgres", "name": "app", "env": "DATABASE_URL" }]` — `engine` is `mariadb`, `postgres`, `mongodb` or `mssql` (installed on the server), `name` 1 to 16 lower-case letters and digits. The panel makes the database and its user and generates the password; the address, password included, goes into the variable you name (a PHP app: into its `.env`). The website's first database is called `site_<website>`, the others `site_<website>_<name>`. **The password is never in the file**, the deploy log, the panel's answers or Tia's. |
| `redis` | every kind but static | `true`, or `{ "env": "REDIS_URL" }` — the website's own Redis, its address in the variable named (`REDIS_URL` unless set). |
| `supabase` | Node.js, Python, Java, PHP | `{ "use": "my-supabase" }` — connects the website to the Supabase app called `my-supabase` on this server, as **Environment → Supabase → Use a Supabase on this server** does: its URL, publishable key, service_role key and database address go into `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_DB_URL`, or the names you give in `"env": { "url": …, "anon": …, "service": …, "db": … }` (`""` leaves one out). **No key is ever in the file.** The server's owner says yes. The file never makes a Supabase — make it first on **Projects → Create → Docker app → Supabase** — and never connects Supabase Cloud, whose keys are typed on the Environment page. |
| `cache.memcached` | every kind but static | `true`, or `{ "memoryMb": 64 }` — a Memcached of the website's own. The server's owner says yes. |
| `cache.varnish` | websites Varnish can serve | `{ "enabled": true, "ttl": 120 }` — Varnish in front of the website, and how long it keeps a page that says nothing. The server's owner says yes. |
| `web.server` | PHP | `"apache"` or `"nginx"` — what serves its PHP (Apache reads `.htaccess`; Apache must be installed). The server's owner says yes. |
| `web.nginx` | every kind | The **Vhost** page's own directives, a line each: `{ "server": ["gzip_comp_level 6;"], "locations": { "root": [], "php": [], "app": [] } }` — through that page's check, nginx's test and its put-back. It needs the website's HTTPS first. The server's owner says yes. |
| `php.mail` | PHP, WordPress | `true` — PHP `mail()` on. The server's owner says yes, after reading the risk its switch states. |

### How you approve what sixpanel.json asks: Update, or the bell

- **Update** on a git website's **Deploys** page lists what the commit's file asks, each line
  ticked. Pressing **Update now** with a line ticked is your yes, kept from that moment — a deploy
  that then fails keeps it. The server's owner says yes to every line. An account says yes, on its
  own website, to a domain, a database and its Redis — and a reseller on its users' websites; a
  Memcached, Varnish, Apache, the Vhost directives and PHP `mail()` are the server owner's alone: shown,
  and waiting for the owner.
- **The Vhost directives** are listed line by line, exactly as nginx will read them, and neither they
  nor PHP `mail()` are ticked for you: read them, then tick.
- **The bell** at the top of the panel (the server's owner): what a git push, Tia, an MCP client or an
  upload deployed, and every line no press ticked, waits there — one card per item. A yes there makes
  it at once. Tia and an MCP client can never say yes for you, and neither can a temporary login.

Once you said yes, the same item applies at later deploys without asking again. A changed item
(another variable name, another memory size) asks again — a new name for a database's variable then
points at the SAME database, never a second, empty one. Something you undid by hand on its page
since (PHP `mail()` turned off, a domain or a database removed, Apache switched off) is **asked
again**: the next deploy never puts it back on the old yes. Taking an item out of the file **never
removes** what it made: the website's **Overview** card says it is no longer in `sixpanel.json`, with
a link to the page that removes it.

**A deploy that does not go live** gives back the variables and the `.env` lines it wrote for the new
version, so the version still live keeps its own database — and if the panel itself stopped in the
middle of that deploy, its next start puts those `.env` lines back (their old values are kept until
then in a file only the server's root can read, never shown anywhere). What was made for it stays (a
database is never dropped) but is **not the website's yet**: the website's **Overview** card says
*made, waiting for its release*, so does the database on the **Database** page (which says when your
app is given its variables, instead of that it has them), and none of its variables (`DATABASE_URL`, `DB_*`, `REDIS_URL`…)
reach your app — the version still live included, when it restarts — until a version whose file asks
for it goes live. That version's build and deploy commands already see it. **A Node.js app's first
version waits for its HTTPS address** ("read, not built yet"): the domain its file names is asked for
at once — in the bell, or added right away when you ticked it — and connecting it builds the version.

### An account's, a temporary login's, Tia's deploy of sixpanel.json

A file is never more than whoever deploys it could do on the website's pages. Deployed by **an
account**, its PHP settings, PHP workers and PHP extensions are not applied (they are the server
owner's on the **PHP** page). Deployed by **a temporary login**, no environment variable is added and
neither the PHP settings and workers nor where PHP keeps its sessions change (those pages are the
owner's or the account's). Neither changes whether a deploy clears Varnish (**Caching**, the owner's).
A **git push** applies what the website's account could set (the owner's own website: everything).
Each such key is listed as ignored, with why and a link to its page; the rest of the file applies, and
the owner's own deploy applies all of it. The same holds on the website's **Settings**: a temporary
login or an account that sends the PHP settings, the PHP workers or the Varnish clearing there is
refused, with the sentence those pages give.

**Deployed by Tia or an MCP client**, a file applies only what Tia's own tools may change without
asking you. What they would ask you first **waits for your yes in the bell**, one card per key showing
exactly what a yes applies, and the rest of the deploy goes live without it:

| Waits for your yes | Why |
|---|---|
| `php.settings` | the **PHP** page's settings are yours |
| `php.pool` | the PHP workers, on the same page |
| `php.sessions` | where PHP keeps sessions — every visitor signs in again; Tia never changes it on her own |
| `java.jvmOptions`, `java.args` | part of the command the app starts with |
| `cron` (a new or changed job) | a scheduled task runs a command; a job the file only drops is removed |
| `env.defaults` that switch a Laravel app's debug pages on (`APP_DEBUG` on, `APP_ENV` not `production`) | debug pages show passwords and keys to whoever triggers an error |

A yes there applies it at once, through that page's own save (a Java app's JVM options and arguments
when it starts next), and is kept: the next deploy by Tia with
the same value applies it without asking, while a changed value asks again.

Everything else goes live with the deploy: `runtime.node`, `runtime.php`, `runtime.java`,
`build.output`, `root`, `env.defaults` (unless they switch debug pages on), `cron` (a job the file only
drops), `php.extensions`, `php.mail`, `node.memoryMb`, `python.memoryMb`, `python.workers`,
`python.startTimeoutSec`, `java.memoryMb`, `java.startTimeoutSec`, `health.path`, `releases.keep`,
`web.maxBodyMb`, `web.spa`, `web.notFound`, `web.headers`, `web.redirects`, `cache.page` and
`cache.varnish.purgeOnDeploy` — and the **build, install and start commands and `afterBuild`**
(`build.install`, `build.command`, `start`, `hooks.afterBuild`), which are never held: they are the
repository's own code, run as the website by every deploy of it. Your own deploy applies the whole
file, and the cards for what it applied go. Tia and an MCP client never say yes to anything a file
asks.

### What sixpanel.json never sets: the firewall, SSH, Cloudflare, another website

The server's firewall, SSH and FTP users, Cloudflare's settings, the panel's own settings, another
website or project, a secret's value, and anything past your account's limits — more than your
package holds, a name another website uses, software the server does not have. Such a key is read and
listed as ignored, with why and a link to the page where it is set.

### Who changed what: sixpanel.json on Activity

Every change a file made is on **Activity**: who deployed (the owner, an account, a temporary login, a
git push with its commit, Tia or the MCP client by name — a rollback, **Build** and Deploy files by
the person who pressed them), which commit or upload, and each key from what it was to what it is now
(for a rollback, what the rollback changed). A variable shows by its name only, and a password in a
start command or a program's arguments shows as `****`. Every yes is there too.

**A rollback** runs the earlier version with what ITS file set — its memory, health check, largest
upload and the rest — not the newer file's; a setting no file set stays as you have it. What waits
for a yes is then what that version's file asks, and a version with no file asks nothing.

## Cron jobs from the file

Each job has a `name` (lower-case letters, digits and dashes, once in the file), a `schedule` (the
same five fields the **Cron jobs** page takes — minute, hour, day of the month, month, day of the
week — or `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`) and exactly one of:

- `"command"` — runs as the website's account in its live version, as a job you add by hand does;
- `"path"` — asks an address of the website itself with a GET, like `"/api/cron/daily"`.

`"enabled": false` keeps a job in the file but switched off.

**A job asking an address** sends `Authorization: Bearer <CRON_SECRET>` when the website has a
`CRON_SECRET` variable, and `User-Agent: SixPanel-Cron/1` — what Vercel Cron sends, so a route
written for Vercel works unchanged. The answer must be 2xx; anything else (a 500, a redirect, no
answer) is a failed run, shown on the Cron jobs page like a command that failed. A Node.js, Python
or Java app is asked on the server itself, at the version that is live — whichever of its two slots
is serving, and before it has a domain; while the app is **stopped** it is asked nothing (each run
says so), until **Start** runs it again. A PHP or WordPress website is asked through this server's
web server at its domain, so it needs its domain with HTTPS first (until then each run fails and
says why).

**The file's list is the list.** At every deploy that goes live, the jobs the file made are made
exactly what the file says now: a new one is added, a changed one is changed, one no longer in the
file is removed. The **Cron jobs** page marks them **From sixpanel.json**. A job you added by hand is
never touched, renamed or removed by the file — even one with the same name. If a later version
has no `sixpanel.json` at all, the jobs the file made go, and what else it set stays as it is.
Rolling back to an earlier version (the **Deploys** page) brings back the jobs of the file that
version had — or none, when it had no file.

## Coming from Vercel (vercel.json)

SixPanel does not read `vercel.json`, but its parts have a place here:

| vercel.json | sixpanel.json |
|---|---|
| `"crons": [{ "path": "/api/cron/x", "schedule": "0 5 * * *" }]` | `"cron": [{ "name": "x", "path": "/api/cron/x", "schedule": "0 5 * * *" }]` — a name is added |
| `"installCommand"` | `"build": { "install": … }` |
| `"buildCommand"` | `"build": { "command": … }` |
| `"outputDirectory"` | `"build": { "output": … }` |
| environment variables in the dashboard | `"env": { "required": [ … ] }`, and their values on the **Environment** page |

Your cron routes that check `CRON_SECRET` keep working: give the website the same `CRON_SECRET` on
its **Environment** page.

## Who wins

For a key the file sets, the file wins at every deploy — over what SixPanel found in your files
and over an earlier change on the page. On the website's **Settings** page such a field is marked
**Set by sixpanel.json**: you can still change it there, and the next deploy puts the file's value
back. Change the file instead. A key the file does not set stays yours, on the page.

## What it never does

Whoever wrote the file — anyone who can push to the repository can — it only sets what the person
who may deploy the website could set on its pages, with the same checks and the same limits
(an account's limits included). It never:

- holds a secret value (see the rule above);
- makes a domain, a database, a Redis or a cache before a yes (see [asked once](#domains-databases-redis-and-caches-in-sixpanel-json-asked-once));
- changes the firewall, Cloudflare, SSH or FTP, the panel's own settings, or another website;
- removes what an earlier file made;
- turns the website into another kind.

A file that carries such a key (`firewall`, `cloudflare`, `ssh`, `secrets`, `sites` …) is still read;
the deploy log and the card list the key, why it was ignored and where it is set. A name written
the wrong way (`domain`, `ssl`, `database`) is pointed at the key that does it.

## Export a website as sixpanel.json

Any website of these kinds can be turned into its `sixpanel.json`, read from the website as it is
now: open its **Overview**, and on the **sixpanel.json** card press **Export sixpanel.json**. The
dialog shows the whole file; **Download sixpanel.json** saves it, **Copy** copies it.

**No secret value is in the file — no password, key or token.** Each secret is named instead:

- one an outside service gave you (a Stripe key, a mail password) is listed in `env.required` — you
  type it where the file is deployed;
- one the app makes up itself (`APP_KEY`, `SESSION_SECRET`, `CRON_SECRET` …) is listed in
  `env.generate` — a new value is made where the file is deployed, never this one;
- the variables SixPanel gives the app (its database's address, its Redis's, its port) are not in the
  file at all: each server gives its own.

A cron job, a header or a Vhost line that holds a password is left out, and the dialog lists it under
**Not in the file** with the page where you set it again. So are SSH and FTP users, extra database
users, the data in the databases (and a WordPress website's posts and uploads — Backups or Move a
website carry those), PHP's disabled functions and free php.ini lines, and the repository's access
token or deploy key. What SixPanel works out from your files by itself (the build and start commands
it detects, the document root) is written only when you changed it on the page — the new server works
it out the same way.

Deploying the file back onto the website it came from changes nothing: its databases are its own (never
a second, empty one), and a cron job you made by hand is not added twice.

## Make a new website from a sixpanel.json

On **Projects → Create**, press **From a sixpanel.json** on the first screen. Paste the file, choose
it, or read it from a repository's branch (a private repository needs its access token, which stays
on this server). Nothing is made yet: you see what kind of website it is, where its files come from,
each secret it names, and what it makes.

- **Secrets**: type each one the file lists in `env.required`. One left empty does not stop anything —
  the website's Overview lists it, and you type it on its **Environment** page later. What
  `env.generate` names is made fresh at the first deploy.
- **What it makes** — each thing the file asks for once (see *asked once* above) is a ticked line, and your press of
  **Make the website** is your yes, kept exactly as a ticked line on **Update** is. The Vhost
  directives and PHP `mail()` are shown and not ticked for you. An account sees what only the server's
  owner may say yes to without a tick: it waits in the owner's bell.

If the file names its repository (`"source": { "git": "https://…", "branch": "main" }`), the website is
connected to it and its first deploy starts by itself — with push-to-deploy on when the file says
`"autoDeploy": true`. Otherwise the file is kept with the website: the first files you upload on its
**Deploys** page are set up by it, and every later deploy whose files carry no `sixpanel.json` reads it
too. A `sixpanel.json` in the website's own files always wins.

A file that cannot make the website — written for another kind, asking for a Node.js or PHP this server
does not have, past your account's package — makes nothing, and the dialog says why.

**Moving a website's setup to another server** is then: **Export sixpanel.json** here, **From a
sixpanel.json** there, type the secrets it names. (Move a website, on the server's own pages, moves the
files and the data too.)

### Keys round 166 added

| Key | For | What it sets |
|---|---|---|
| `hooks.deploy` | PHP, Node.js, Python, Java | The **Settings** page's deploy commands, one line each: `["php artisan migrate --force"]` (20 at most). |
| `keep` | PHP, Python, Java | Files and folders kept between deploys, like `["storage/"]` — kept from the deploy that first names them. |
| `env.generate` | Node.js, Python, Java, PHP | Secrets made fresh where the website has no value — an empty `APP_KEY=` too. The value never appears in a log or on a page but **Environment**. |
| `web.rewrites` | every kind | **Web Rules** Rewrites: `[{ "from": "/shop", "to": "/store", "match": "prefix" }]` — the address stays, another path answers. |
| `web.blockPaths` | PHP | Refused addresses, like `["/install"]` — a 404 before PHP sees them. |
| `web.frontController` | PHP | `true` sends an address with no file to `index.php`; `false` answers 404. |
| `node.mode` | Node.js | `"server"` or `"static"`. |
| `python.server` | Python | `"gunicorn"`, `"uvicorn"` or `"custom"`. |
| `python.appModule` | Python | Like `"mysite.wsgi:application"`. |
| `python.settingsModule` | Python | Django's settings module, like `"mysite.settings"`. |
| `python.collectstatic` | Python | `false` skips collectstatic. |
| `java.mainClass` | Java | Like `"com.example.App"`. |
| `wordpress.security` | WordPress | `{ "xmlrpc": false, "userEnum": false, "loginLimit": true, "fileEdit": false }` — the Settings page's switches. |
| `name` | — | What a website made from the file is called. Read when it is made, never at a deploy. |
| `source` | — | `{ "git": "https://github.com/owner/repo.git", "branch": "main", "autoDeploy": true }` — where a website made from the file takes its files. Never a token. Read when it is made, never at a deploy. |

A redirect or a Rewrite can say when it applies, as Web Rules' condition does —
`"when": { "type": "device", "value": "mobile" }` (type `uri`, `query`, `host`, `country` or `device`;
`op` `eq`, `ne`, `re` or `nre`) — and `"enabled": false` keeps a rule switched off. A header's
`"always": false` sends it on a good answer only. A deploy by Tia or an MCP client applies all of these.

## Check sixpanel.json before you deploy

The website's **Overview** has a **sixpanel.json** card: which version's file is in force, what it
set, what it asked for (made with a yes, waiting for one, or already there), what it ignored and
which variables it needs are still missing. **Check sixpanel.json** reads
the file the next deploy would read — from the newest commit of the repository's branch for a git
website, or from the last upload — and shows what it would change, without deploying anything.

## Examples, one per kind

### Static website

A site built with Vite into `dist/`:

```json
{
  "version": 1,
  "kind": "static",
  "runtime": { "node": "24" },
  "build": { "command": "npm run build", "output": "dist" }
}
```

### Node.js app

An Express app with a cleanup job every 15 minutes:

```json
{
  "version": 1,
  "kind": "node",
  "build": { "install": "npm ci", "command": "npm run build" },
  "start": "node dist/server.js",
  "env": { "required": ["SESSION_SECRET"], "defaults": { "NODE_ENV": "production", "LOG_LEVEL": "info" } },
  "cron": [{ "name": "cleanup", "schedule": "*/15 * * * *", "command": "node dist/cleanup.js" }],
  "hooks": { "afterBuild": "npx prisma migrate deploy" }
}
```

### PHP website

A Laravel app: served from `public/`, PHP 8.4 with `intl`, the scheduler every minute:

```json
{
  "version": 1,
  "kind": "php",
  "runtime": { "php": "8.4" },
  "root": "public",
  "env": { "required": ["APP_KEY"], "defaults": { "APP_ENV": "production", "APP_DEBUG": "false" } },
  "cron": [{ "name": "scheduler", "schedule": "* * * * *", "command": "php artisan schedule:run" }],
  "hooks": { "afterBuild": "php artisan migrate --force" },
  "php": { "settings": { "memory_limit": "512M", "upload_max_filesize": "64M", "post_max_size": "64M" }, "extensions": ["intl"], "mail": false }
}
```

### WordPress

A theme or plugin repository that also sets the site's PHP:

```json
{
  "version": 1,
  "kind": "wordpress",
  "runtime": { "php": "8.3" },
  "php": { "settings": { "memory_limit": "256M", "max_execution_time": 120 }, "extensions": ["imagick"] }
}
```

### Python app

A Django app started by gunicorn with three workers, and a nightly job:

```json
{
  "version": 1,
  "kind": "python",
  "build": { "install": "pip install -r requirements.txt" },
  "start": "gunicorn mysite.wsgi:application --workers 3",
  "env": { "required": ["DJANGO_SECRET_KEY"], "defaults": { "DJANGO_SETTINGS_MODULE": "mysite.settings" } },
  "cron": [{ "name": "clearsessions", "schedule": "0 3 * * *", "command": "python manage.py clearsessions" }]
}
```

### Java app

A Spring Boot app built with Maven:

```json
{
  "version": 1,
  "kind": "java",
  "runtime": { "java": "21" },
  "build": { "command": "./mvnw -q -DskipTests package", "output": "target/app.jar" },
  "env": { "defaults": { "SPRING_PROFILES_ACTIVE": "prod" } }
}
```

### The whole website in one file (Node.js)

```json
{
  "version": 1,
  "kind": "node",
  "start": "node .next/standalone/server.js",
  "node": { "memoryMb": 1024 },
  "health": { "path": "/api/health" },
  "releases": { "keep": 5 },
  "web": {
    "maxBodyMb": 50,
    "headers": [{ "name": "X-Robots-Tag", "value": "noindex" }],
    "redirects": [{ "from": "/blog", "to": "https://blog.example.com", "status": 301, "match": "prefix" }]
  },
  "domains": [{ "name": "shop.example.com", "primary": true, "www": true }],
  "databases": [{ "engine": "postgres", "name": "app", "env": "DATABASE_URL" }],
  "redis": { "env": "REDIS_URL" }
}
```

The first **Update** lists the domain, the database and the Redis, each ticked; pressing
**Update now** makes the database before the app's migrations run, and the domain with HTTPS
once the new version is live. Every later deploy asks nothing about them.

## A worked example: allsweb.com (Next.js)

allsweb.com is a Next.js app (`output: 'standalone'`) with three Vercel-style cron routes that check
`CRON_SECRET`. Its `sixpanel.json` (the real one names every variable the site reads; five are shown):

```json
{
  "$schema": "https://installer.allsweb.net/sixpanel/sixpanel.schema.json",
  "version": 1,
  "kind": "node",
  "runtime": { "node": "24" },
  "health": { "path": "/api/health" },
  "env": {
    "required": ["NEXT_PUBLIC_APP_URL", "NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", "SUPABASE_SERVICE_ROLE_KEY", "CRON_SECRET"],
    "defaults": { "NODE_ENV": "production" }
  },
  "cron": [
    { "name": "infra-daily", "schedule": "30 0 * * *", "path": "/api/cron/infra-daily" },
    { "name": "infra", "schedule": "0 12 * * *", "path": "/api/cron/infra" },
    { "name": "agent", "schedule": "15 6 * * *", "path": "/api/cron/agent" }
  ]
}
```

There is no `build` and no `start`, on purpose: SixPanel reads both from the project itself. Its
`packageManager` (`pnpm@10.14.0`) runs through npx at exactly that version, and `output: 'standalone'`
starts as `.next/standalone/server.js` with `.next/static` and `public` copied beside it. Writing them
in the file is allowed — but a plain `pnpm` there needs corepack, which Node.js 25 and later no longer
ship.

On a server that runs its own Supabase (a Docker app called `supabase` here), one more key replaces
typing the three Supabase values by hand — the panel reads them from that app, and follows it when its
keys or address change:

```json
  "supabase": { "use": "supabase" }
```

At its first deploy the log's sixpanel.json section says the health path, the Node.js line and the
three cron jobs it adds, and which variables are missing until they are typed on the **Environment**
page; `NODE_ENV` is added because the website has no value for it. Node.js 24 must be installed on the
server first (**Software → Node.js**) — the deploy says so if it is not. A new version goes live only
once `/api/health` answers. A second deploy of the same file changes nothing; changing one job's
schedule in the file changes exactly that job at the next deploy.

## The JSON Schema

Editors such as VS Code check the file as you type when its first lines name the schema:

```
"$schema": "https://installer.allsweb.net/sixpanel/sixpanel.schema.json"
```

The panel carries the same schema: the **sixpanel.json** card on a website's Overview links to it.
The schema describes version 1; SixPanel itself is what checks the file at a deploy.

## How to check it worked

- The deploy log has a **sixpanel.json** section, and it says what you expected.
- The website's **Overview** card names the version whose file is in force.
- **Cron jobs** lists the file's jobs marked **From sixpanel.json**; **Run now** on a job that asks an
  address shows the answer's status in its log (`HTTP 200`).

## If it went wrong

- **"sixpanel.json: … the deploy stopped before anything changed"** — the file says something wrong;
  the message names the key. Fix the file and deploy again. The live version was never touched.
- **"is written for a Node.js app … and this is a PHP website"** — `kind` names another kind. A
  website is never converted: make a website of that kind, or correct `kind`.
- **"asks for Node.js 24, which is not installed"** — install it on **Software → Node.js** (the server's
  owner), or remove `runtime.node` from the file.
- **A cron job that asks an address fails with "not run: this website has no domain that answers
  over HTTPS yet"** — a PHP or WordPress website is asked at its domain: add it on **Domain & SSL**.
- **"HTTP 401"** in a job's log — the website has no `CRON_SECRET`, or not the one the route expects:
  set it on the **Environment** page.
- **A variable is still missing** — the card on the Overview lists it; type its value on the
  **Environment** page (the file never holds a secret's value).
- **"waits for a yes"** — a domain, a database or another item the file asks for has no yes yet: tick
  it the next time you press **Update**, or answer it in the bell (the server's owner).
- **"…this website's account has 768 MB for all its websites"** (or its PHP workers, or its PHP
  memory) — the file asks past the account's package; lower the value in the file, or ask the
  server's owner for a larger package.
