# Staging & Testing

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

Part of: SixPanel documentation

**What this page is for:** a **staging site** is a private copy of a project where you
try changes safely before they go live. It is on an address of its own, so you can try an
update, a plugin, a theme or a big change there first. The copy lives inside the project
it was made from: the project's own **Staging & Testing** page lists its copies. When you
are done, delete it: everything it had is gone. (The page was called Staging before, and
earlier versions called a staging site a test copy.)

**You need**

- An address for the copy — one of:
  - **a temporary address** (`https://…sixpanel.dev`), which needs this panel
    connected to your AllsWeb account (**Settings → AllsWeb account**): AllsWeb
    creates and deletes its DNS record for you; or
  - **your own domain** (`staging.example.com`), which needs no AllsWeb account.
- An email address for certificates (**Settings**), the same one every HTTPS
  address on this server uses.
- Enough free disk space for a second copy of the project's files and database.
  The panel checks before it starts and says how much it needs.

---

## Which projects can have a staging site

The four kinds the panel can start by itself:

| Project | Staging site |
|---|---|
| **6amMart** shop | Yes — everything is automatic |
| **WordPress** website | Yes |
| **Laravel** app (a PHP website with an `artisan` file and its packages installed) | Yes, when its database is on this panel |
| **Static** website | Yes |
| **A custom app** — another PHP app, a Node.js app | **Not from this page.** **AI Coding** can make one and set it up, and you start it — or you set up a second website by hand. See [A custom app: no staging site](#a-custom-app-no-staging-site) |
| **A Create with AI** website | **No.** See [A custom app: no staging site](#a-custom-app-no-staging-site) for what to do instead |

One rule holds for every staging site: **it never reaches production's database, files,
queue or customers.** The panel does not take that on trust. Before a staging site
answers a single request, the panel **asks the app itself** which database it uses, and
starts it only when the answer is the staging site's own. An app the panel cannot ask
is never started by the panel: this page offers it no staging site, and one that AI
Coding sets up is started by your own press, nobody else's.

## How to use a staging site, in short

1. Open the project, then **Staging & Testing**, and press **Create staging site**.
   Choose a temporary address or your own domain, press the button, and wait for the
   job.
2. The panel copies production's database and files, points the copy at its own
   database, switches its outgoing email off (and, for 6amMart, payments, SMS and push
   notifications), **asks the app which database it uses**, and starts it. Its card then
   says **Started by the panel**.
3. Press **Open** and test: install the update, the plugin, the theme; place a test
   order.
4. Want production's newest data in it? Press **Refresh from production**. What you
   changed on the staging site is replaced.
5. Take what you tested to production. With git: **Promote**
   ([Working on a branch and promoting](#working-on-a-branch-and-promoting)). Without
   git: make the same change on production — the same ZIP, the same update
   ([Without git](#without-git-taking-a-tested-change-to-production)).
6. Done? Press **Delete staging site**.

If a card says **Not started**, it also says why and what to do. Usually: put right
what it names and press **Look again**. Nothing you press can start a staging site that
still points at production.

## The Staging & Testing page

Every project has a **Staging & Testing** page in its own menu. For a custom app that
page has no **Create staging site**: it says why, what to do instead by hand, and — for
a PHP or Node.js app — that **AI Coding** can set one up, with the link to it.

- On the **production** project it lists that project's copies, one card each: the
  copy's name and label, where it stands (being made, ready, refreshing…), every
  address it answers on, when it was made and last refreshed, and its switches —
  with **Open**, **Refresh from production**, **Copy settings** and **Delete staging
  site**. **Create staging site** is at the top.
- Each card also says whether the copy is **started**. A copy that is not carries the
  mark **Not started** beside its name, with the reason, and has no **Open** (its
  address would only say it has not been started). A started copy shows one quiet
  line — "Started by the panel" (or, on a staging site of a custom app,
  "Started by you on" a day or "Running since before the last update")
  — and what the panel found is folded under it.
- On a **copy** the same page shows that one copy: what it is a copy of, the same
  card, and **Open production**.

On **Projects** and on **Home** a copy is not a card of its own: it is a row inside
the card of the project it was made from — its short name (which opens the copy),
where it stands, and **Open**. Making one is done inside the project: its
**Staging & Testing** page, and its **Overview**, which has **Create staging site** and
a link to this page beside it. (A custom app's Overview has the link only.)

A copy's card also has **Connect your own AI agent to this copy**: it opens that copy's
own **AI agent** page, so the connection made there reaches the copy and nothing else —
see **[Connect your own agent](https://www.allsweb.com/sixpanel/docs/ai-assistant#connect-your-own-agent)**.

## Make a copy

1. Open the project, then **Staging & Testing** (or its **Overview**).
2. Press **Create staging site**.
3. The dialog shows what is copied and the copy's short name (the project's name
   with `-t1`, `-t2`… on the end), and asks **where the copy answers** — see the
   next section. Choose the switches (see below), give it a label if you like
   ("checkout redesign"), and press **Create staging site**.
4. The log opens and shows each step. A small website takes a minute or two; a
   6amMart shop with a large database and its customer website a few minutes more.

The dialog also says **when it starts**, for the kind of project you are copying: what
the panel asks the app before it starts the copy.

When it finishes, the copy is on the project's **Staging & Testing** page and in its
card on **Projects**, and every page of the copy says **Staging site of … — changes here
never touch production** at the top, with its address. While a copy is **not started**
that banner says **— not started** instead, with the reason and a link to its
Staging & Testing page.

### Where the copy answers

**A temporary address.** AllsWeb gives the copy a name under `sixpanel.dev` — one
for a website; for a 6amMart shop, `dash-…` for the admin, plus a name for the
customer website and one for live updates when the shop has them. There is nothing
to set up, and the name is deleted with the copy. When this panel is not connected
to your AllsWeb account the choice is still shown, greyed out, with the link to
connect it.

**Your own domain.** Type the name the copy should answer on:

- a website: one name, for example `staging.example.com`;
- a 6amMart shop: a name for the admin panel (`dash-staging.example.com`), and a
  name for the customer website when the shop has one (`staging.example.com`). Live
  updates get a name of their own beside the admin panel's — `ws-staging.example.com`
  — which the panel composes and sets up; you do not type it.

The name is connected exactly as any domain on this server is (see
[Domain & SSL](https://www.allsweb.com/sixpanel/docs/domain-ssl), and for a website
[its Domain page](https://www.allsweb.com/sixpanel/docs/websites-and-apps#domain-ssl)):

- With your Cloudflare account added to the panel and the domain in it, the panel
  creates the DNS record for you — **proxied** (orange cloud), like every record it
  makes. Otherwise create the record yourself, pointing at this server, before you
  press the button: a name that does not reach this server yet stops the copy with
  the same message the Domain page gives, and **Refresh from production** on the
  copy finishes it once the name points here.
- The certificate is Let's Encrypt's.
- A name must be free: not one another project or website on this server answers
  on, and not the panel's own.

A copy on your own domain is still a copy: search engines are told not to index it
and nothing on it is served from the server's cache.

**Later, on an existing copy.** A copy made on a temporary address can get your own
domain afterwards: add it on the copy's own **Domain** page (press **Use your own
domain** on its card), connect it, and make it the primary. The copy then answers on
both. **Give the temporary address back** on its card removes the temporary name,
issues the certificate again without it and has AllsWeb delete it — the copy keeps
your domain. Removing the temporary name on the Domain page does the same. A copy
always keeps one address: the last name it answers on cannot be removed — add another
domain first, or delete the copy.

### What a copy is

A copy is a whole project of its own, not a view of the original:

- its own **database** — a copy of production's, taken as one consistent snapshot
  (production is never locked or paused while it is read). A Laravel app whose database
  is **not** on this panel is offered no staging site: add its database on the website's
  **Database** page first;
- its own **files** — production's as they are live, with the dependencies and
  build output made again on the copy (`node_modules`, the Next.js build, Laravel's
  caches and logs are not copied);
- its own **account, PHP pool, Redis, services, scheduled tasks and certificate**;
- its own **address**: one name for a website; for a 6amMart shop, a name for the
  admin, plus a name for the customer website and one for live updates when the
  shop has them (see *Where the copy answers* above).

### What the panel changes in a copy

What it does to point the copy at its own database depends on the kind of project:

| Kind | What the panel changes |
|---|---|
| **6amMart** | It writes the copy's own database, cache and address into the copy's `.env` before the copy's files are put in place. The customer website is built again for the copy's address, so its pages call the copy, never the shop. |
| **WordPress** | It sets the database constants in `wp-config.php` and points WordPress at the copy's address (every address WordPress stored). Production's object cache is not shared: `wp-content/object-cache.php` — the file a Redis or Memcached cache plugin installs — is not copied, and the copy's cache keys get a prefix of their own (`WP_CACHE_KEY_SALT`, `WP_REDIS_PREFIX`). To test with such a cache, switch it on again in the staging site's own plugin page. |
| **Laravel app** | In every text file under 2 MB it replaces production's database **password, user and name** with the copy's — the exact strings, and nothing else. Production's **address** is replaced only in `.env` files; every other file that names it is counted and shown, not touched. |
| **Static website** | Nothing: it has no database. Its pages may still call production's API from the staging address — the panel cannot see or change what a page calls. |

Changing a file is not the same as knowing the app uses it. A `.env` on the disk proves
nothing — the panel writes one into every PHP website itself. That is why a copy is
started only on the app's own word.

## Started, or not started

Every staging site is one of the two, and its card says which.

**Not started** means:

- its address answers one sentence — *This staging site has not been started.* (or
  *…is being made*, *…is being refreshed*, *…is being updated*) — whatever is on its
  disk. Only the certificate check still gets through, so a copy on your own domain can
  get its certificate while it waits;
- **nothing of its app runs**: no PHP, no Node.js process, no scheduled task, no deploy
  command, no WP-CLI, no queue worker;
- its **Terminal** and **SSH** still work — that is how you put right what its card
  names. They open with a warning, and an AI agent's guide begins with it: "NOT STARTED: this app may still be
  connected to PRODUCTION's database. Run nothing of it until its owner has started the
  staging site."

A copy is born not started. A job that stops half-way, or a panel that restarts, leaves
it exactly so.

### Started by the panel — four kinds, and only on the app's own word

| Kind | What the panel asks before it starts the copy |
|---|---|
| **6amMart** | The copy's own code, run as the copy's account: every database connection, the Redis it uses, the queue, the file store and its bucket, the mailer. Anything that is not the copy's stops the job. |
| **WordPress** | WP-CLI reads `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DB_HOST` out of `wp-config.php` without loading WordPress; there must be no `wp-content/db.php`; only then is the database itself asked which one it is. |
| **Laravel** (an `artisan` file and `vendor/laravel/framework`) | Its default connection's database and user are the copy's, then the database itself says so; its queue, default disk, cache and Redis name nothing outside this server. |
| **Static website** | Nothing to ask: it has no database on the panel. |

For WordPress, Laravel and a static site one more thing must be true: the panel
searched the copy's **whole folder** — every folder, every file name, binary files too —
and found production's database password **nowhere**.

If any of that fails, the copy stays **not started** and its card says why: the app
could not be asked, it named another database, it names a queue or a disk outside this
server, a file still holds production's password. Put right what the card names and
press **Look again**, or press **Refresh from production**. **No press of yours starts
a staging site of these four kinds — only the app's own answer does.**

### A custom app: no staging site

**Another PHP app, a Node.js app and a Create with AI website are not offered a staging
site on the Staging & Testing page.** Their **Staging & Testing** page says so, with the
reason, and has no **Create staging site**. Tia, a connected AI agent and the `sixpanel`
command get the same answer.

Why: such an app keeps its database settings wherever its author chose — a file of any
name, a variable, a line of code, a value it decrypts when it starts — and there is
nothing the panel can ask it. A copy the panel had made half-way is exactly the thing
that ends up connected to production's database. So the panel never makes one and
starts it by itself.

That leaves two roads, and they are different:

| Road | What you get |
|---|---|
| **By hand** | **No staging site.** A second website of your own, which you set up yourself — the five steps below. |
| **AI Coding** (a PHP or Node.js app) | **A staging site**, made and set up by AI Coding — and **started by you**: you press **Start the staging site**. See [AI Coding sets one up](#ai-coding-sets-one-up). |

A **Create with AI** website has the first road only: it has no AI Coding page — its
**Build with AI** page, with its own preview and **Publish**, is where its changes are
tried.

**By hand: a second website of your own, set up by you.**

1. Open **Projects** and press **Create a new project**. Add a website of the same
   kind, on an address of its own (`staging.example.com`), with a database of its own.
2. Put the code on it: connect the same git repository (another branch, if you like),
   or upload a ZIP of the code.
3. Copy the data: on **production's Database** page press **Export now** and download
   the file; on the **new website's Database** page press **Import** and choose it.
4. Give the app the **new website's** database — its name, user and password are on its
   Database page — in the place where your app keeps them: its `.env`, its config file,
   its variables. Change the keys it holds for email, SMS, payments and storage to test
   values.
5. Test there. When it works, make the same change on production: deploy the same
   commit, or the same ZIP.

Nothing ties that website to production. It is not refreshed from it, not promoted to
it and not deleted with it: it is a website of its own.

The same answer is given to a website whose **database is not on this panel**: a staging
site would get no database of its own, so whatever its code connects to is what
production connects to. Add the database on the website's **Database** page first.
(A PHP or Node.js custom app whose database is not on this panel can still get a
staging site from AI Coding — with no database of its own. Its set-up begins with
exactly that warning, and a second database has to be made where production's is
before you start it.)

### AI Coding sets one up

For another PHP app or a Node.js app, **[AI Coding](https://www.allsweb.com/sixpanel/docs/ai-coding)** (in the project's
own menu; it needs SixPanel Pro) works on a staging site first, and makes one when the
app has none:

1. Open the project's **AI Coding** page, leave **Staging site** chosen, and write what
   you want changed. With no staging site yet, AI Coding says it will make one. It gets a
   temporary address when your AllsWeb account gives one — otherwise you are asked for
   a domain of your own — and its own git branch when production's code comes from a
   repository.
2. The staging site is made **not started**: the panel copies the files and the
   database, and its address answers *This staging site has not been started.* Nothing
   of the app runs.
3. **The session's first job is the set-up.** It is handed what the card
   [Set up by hand](#set-up-by-hand) shows you — the staging site's own database
   (never a password in the text), the files the panel changed, and everything that
   still points at production — puts the staging site's database where your app keeps
   it, and asks the panel to **look again**. It is told to run nothing of the app while
   it does.
4. When the panel's search of the whole folder, and of every variable, deploy command
   and scheduled task, finds nothing that still points at production, the session shows
   **Start the staging site**. **That press is yours.** It is the same job as on the
   Staging & Testing page: the panel searches once more, and refuses with the list if
   anything still points at production. The dialog asks for the same word as the tick on
   the card: start it only if the app uses this staging site's own database and no other.
   **Where the panel could not look, the session shows no Start** — the app's database
   is not on this panel, or production's password is one a search cannot find. The
   session tells you what comes first (a second database made where production's is,
   or your own look through the app's settings), and you start the staging site on its
   **Set up by hand** card, with the tick.
5. Then what you asked for is done on the staging site.

What stays true whoever set it up:

- **Only you start it.** Not AI Coding, not Tia, not a connected AI agent, not the
  `sixpanel` command, not a temporary login. Asking the panel to look again is the one
  thing a session may ask for, and a look never starts a staging site of a custom app.
- It is on the project's **Staging & Testing** page like any other staging site, with
  the card **Set up by hand** under it — which says that AI Coding made it, and links to
  the project's AI Coding page. You can go through the five parts yourself instead, and
  press **Start the staging site** there.
- **What the panel cannot see, the session cannot promise**: a password the app keeps
  encrypted or loads from somewhere else, and its keys for email, SMS, payments and
  storage. The session is told to look for them and to tell you what it found and
  where. Read that before you start the staging site, and before you test anything
  that sends mail or takes money.
- **Refresh from production**, **Delete staging site** and — on a branch — **Promote**
  work on it as on any staging site.

### Who may start one

A staging site of the four kinds: the panel, on the app's own answer. A staging site of
a custom app — one AI Coding set up, or one made on an earlier version: **its owner**,
with the press **Start the staging site**. **Look again** may be pressed by anyone who
may read the staging site.

## Staging sites of a custom app: set up, then started by you

**A new staging site of a custom app is made by AI Coding and from nowhere else** — see
[A custom app: no staging site](#a-custom-app-no-staging-site). This section is about
the ones that exist: one AI Coding made, and one made on an earlier version.

Earlier versions made a staging site of any website. The ones that exist are **kept**:
the project's **Staging & Testing** page still lists them, and says where another one is
made.
The panel never starts them. They are searched, and started on their **owner's**
word, as this section describes, until you delete them. Not AI Coding, not Tia, not a
connected AI agent, not the `sixpanel` command inside the copy and not a temporary
login can start one.

The same card is shown for a staging site whose app has stopped being one the panel can
ask — a Laravel app whose `artisan` file or packages were removed, for example.

### Set up by hand

The card **Set up by hand** sits under the copy's own card on the **Staging & Testing**
page. It has five parts; work through them in order.

**1. This staging site's database.** Its name, user, host, socket and port, each with a
**Copy** button, and **Show password** (the password is fetched only when you press
it). Put these where the app keeps its database settings. If production's database is
not on this panel the card says so instead: "This app's database is not on this panel,
so this staging site has none from the panel. If its code connects to one, it would
connect to the SAME one as production." Make a second database where production's is,
and give its address to the staging site before you start it.

**2. Changed for this staging site.** The files in which the panel put the copy's
database. **Look at them.** If the app keeps its database settings anywhere else, change
them there too. Each file is a link to the copy's **Files** page. For a Node.js app or
a Create with AI website the card says that no file was changed: the copy's database is
in its variables.

**3. Still points at production.** Three lists, each entry a link to where it is
changed:

- **files that still hold production's database password** — these refuse a start;
- **variables, deploy commands and scheduled tasks** that hold production's password,
  name its database or its address — these refuse a start too;
- then, **shown only**, how many files name production's database or its address.
  They decide nothing.

Where the panel **could not look**, the card says that — never "nothing found": a
password it cannot tell from an ordinary word (shorter than eight characters, or the
same as the user or the database's name), a database it does not know, or a search that
did not finish. A file of a **release** (Node.js, static, Create with AI) is not put
right on **Files**, because the next deploy replaces it: put the setting into a
**variable**, or correct it in the repository and deploy.

**4. What the panel cannot see.** A password the app keeps encrypted or loads from
somewhere else, and the keys it holds for email, SMS, payments and storage. They are as
they were copied from production. Change them in the app before you test anything that
uses them.

**5. Start it.** Tick **This app uses this staging site's database and no other** and
press **Start the staging site**. The button is lit only once the box is ticked, and is
not shown while a job is still changing the copy. The press is a job:

1. the panel searches again;
2. a password found, a search that did not finish, or a variable, command or task that
   still points at production **refuses** the start — with the list and the way on;
3. otherwise the copy is started, and its card says **Started by you on** that day;
4. what the create left undone runs now: a Node.js build and its deploy commands, the
   branch's code where the copy follows a branch, WordPress's change to its new address.

**Look again** runs the search — and, for the kinds the panel can ask, the questions —
without starting anything. Press it after you changed a file.

Your word stands for later versions too. Every refresh and every deploy is searched
again (next section), and where the app hides its password, keep that file on **Kept
between deploys** in the copy's **Settings**.

## What is asked again

- **Refresh from production.** Its first step marks the copy **not started**. The new
  files are searched before they are put in place. The files that hold the copy's own
  database password are carried over — except a settings cache the app made itself
  (Laravel's `bootstrap/cache/`): the app reads its settings from its files again. Then
  it is started again by the panel's own
  questions. (A staging site of a custom app, which you
  started yourself, is started again when the search is clean.)
- **Every deploy of a copy** — a push, **Deploy the copy**, a ZIP, a roll-back, a
  restore. The version about to be placed is searched before anything of it runs or
  reaches the live folder. A password in a file that is not the copy's own refuses it
  there: "config.php holds production's database password — this staging site would
  use production's database. Keep the file on the server (Kept between deploys) or take
  the password out of it." After a deploy of a Laravel copy the app is asked again.
- **Every save** of a variable, a deploy command or a scheduled task on a copy that
  holds production's password is refused, with the reason.
- **A 6amMart copy does not answer while it changes hands**: a refresh, every update of
  the copy, an import and a restore into it start by marking it not started and end by
  starting it — after its `.env` was written, "Payments, SMS and push off" ran and the
  app was asked. A job that stops leaves it not started, and its card says why, lists
  what was found (the file that holds production's password, or what the app answered)
  and has **Look again**, **Refresh from production** and **Delete staging site**. The
  job itself names the way on. After a **refresh**: put it right on production and
  refresh again. After an **update** of the copy: correct the branch it deploys and
  deploy again — a refresh would only deploy the same commit again. After an **import**
  of a database from an older version: run **Migrations**, then press **Look again**.
  An update that leaves the staging site not started is listed as a deploy that
  **failed**, never as one that finished.
- **Look again on a staging site that is answering.** The app is asked again. If it now
  names another database — someone changed its `.env` or `wp-config.php` by hand — or
  can no longer be asked at all, the staging site is **stopped**, and its card says what
  the app answered. Put the file right and press **Look again**: it is started again on
  the app's own answer. (A staging site of a custom app,
  started by its owner, is stopped by a look only when a file holds production's
  database password.)
- **A Look again that waited behind another job.** If a refresh or a deploy changed the
  staging site after you pressed, the job stops with "…was changed by another job… Press
  it again." It would otherwise build a version it never looked at.
- **Scheduled tasks wait.** While a staging site is not started, none of its scheduled
  tasks runs: each row says **Waits — this staging site is not started**, and **Run
  now** is answered with the reason. They run again once it is started.
- **Migrations.** `sixpanel db migrate` and the **Migrations** button run on a staging
  site that is not started only when the one reason is that the app could not be asked
  — an older database that needs its migrations before the app can answer. For any
  other reason they are refused: a migration runs on whichever database the files name.

### A database you put into a staging site yourself

**Refresh from production** is the way to bring production's data into a staging site.
But a file can also be put in by hand — **Database → Import**, a **restore** of a backup,
`sixpanel db import` in a shell — and that file is, more often than not, production's own
export: its live payment gateways, SMS and push settings, and your customers' phones are
in it.

**A 6amMart staging site** is therefore handed the same care whichever of the three you
use:

1. it is marked **not started** before the first statement runs — its addresses answer
   "This staging site is being updated." and none of its processes run, its queue worker
   and scheduler included;
2. the database as it was is kept first, as for every import;
3. after the file has run, **Payments, SMS and push off**, the S3 and reCAPTCHA rules and
   the mail switch are applied again to what arrived, by the staging site's own switches;
4. the app is asked which database it uses, and only then is the staging site started;
5. live updates are the staging site's own again: where it has them, its own address is
   written back into its settings; where it has none, the switch the file brought with it
   is turned off — the file says they are on, at production's address.

**Database → Import runs as a job.** The page answers as soon as your file has arrived,
and a card at the top of the Database page shows the import step by step: the staging
site stopped, the database kept, your file, and what the file left to do. You can leave
the page — the job goes on, and **Activity** keeps its log.

Steps 3 to 5 are **a second job**, started by the first the moment the file has run — or
has failed: on a shop with many uploads they take a minute or two. The same card shows
that job next (its log is named "Staging site: payments and email off again, the app
asked, then started"), and the staging site stays not started until it has ended well.
If it does not, the reason is on the staging site's card. A file from an older 6amMart
may need `sixpanel db migrate` before the app can answer — run it, then press **Look
again**.

- An import is **refused while a job is changing the staging site** (a refresh, an
  update): wait for that job, then run it again. Nothing is imported.
- `sixpanel db import` asks the panel to do steps 1, 3 and 4, and follows that job to its
  end. If the panel is not running, an import into a staging site is refused before
  anything is changed — start the panel first. A project that is not a staging site is
  imported as it always was.
- If the import or the restore **fails half-way**, the staging site stays not started.
  **Look again** — and the next update of it — apply the rules of step 3 before they
  start it, so a staging site is never started on rows nobody made quiet.
- **Look again** pressed while a file is still being imported does nothing until the
  file has ended: half a file is not looked at, and an update of the staging site that
  runs meanwhile does not start it either.

**A WordPress staging site** is **stopped before an import runs** — its address says
"This staging site is being updated." — because the rows of production's export name
production's address: its sign-in page would otherwise send you to production's admin.
After the file has run, WordPress is asked which database it uses, moved back to its
own address, and only then does the address answer again. If the import fails half-way,
the staging site stays not started: press **Look again**, and the same happens before
it answers.

Every time a WordPress staging site is started — when it is made, after a refresh, by
**Look again** — it is moved to its own address **before** that address is opened. While
the move runs, the address still says "has not been started". A WordPress that cannot
be asked is not moved and is not started.

**A Laravel app**: the panel changes no row of an app it does not know. What a file
brings — a payment key, a mail server — is yours to switch off before you test.

**phpMyAdmin, or any database tool you open yourself, is by hand.** An import made
there is not seen by the panel, so none of the steps above run. On a staging site, use
**Database → Import**.

### Nothing starts a staging site's processes while it is not started

A 6amMart staging site that is not started has no queue worker, scheduler, websocket
server or customer website running, and none of them can be started from its **Services**
or **Domain & SSL** pages, by another job, or after a server restart: the press is
answered with the reason and the way on (its card on the **Staging & Testing** page).
They start when the staging site does.

A staging site can be started while **its customer website stays stopped**: when the
storefront was built with production's address in it, the card says so — "The customer
website of this staging site is not started", with the files — and its admin side
answers. Update the storefront of the staging site, or press **Refresh**: both build it
again for the staging site's own address. Until then nothing else starts it — its
**Services** page and a change of its domain answer with the same reason — because an
order placed on that build would be a real order in production. (Where the search of
the build did not finish, the card says that instead: press **Look again**.)

### The staging site's own database settings never travel

A file that holds **the copy's own database password** is the copy's connection file,
whatever its name. The panel finds it by what is in it, so there is no list to keep:

- a deploy of the copy does **not replace** it (the log says when the repository has a
  different version);
- a refresh **carries it over**;
- it is **never listed** as changed on the git card and **never committed**;
- it is **never placed on production**. Every deploy of a project that has staging
  sites — a push, an **Update**, a ZIP, the deploy inside a promote — searches the
  version for each copy's database password first and refuses, naming the file. A
  production that ran on a staging database would lose it at the next refresh.

For the same reason **Refresh** and **Delete** look for the copy's password in
production's live folder and variables before the copy's database is dropped. If it is
there, the job stops with *production is running on this staging site's database* and
drops nothing.

And what **arrived from production** with the copy — uploads in a folder the panel has
no name for, a tracked file that was edited by hand on production — is not a change you
made: it is left out of every list and every commit, and the git card counts it under
*came from production*. To bring such a hand edit into the repository, change the file
again on the staging site (it is then listed) and commit it there.

## Staging sites made before this update

A copy made by an earlier version keeps answering — of whatever kind it is, a custom
app's too. The first time the updated panel starts, one job per such copy looks at it:

- production's database password is found in it → the copy is **stopped**, and its
  banner says "Stopped by the update: config.php still holds production's database
  password" (with the file's own name). Put the file right, then on its
  **Staging & Testing** page press **Look again** (WordPress, Laravel) or
  **Start the staging site** (a custom app — see [Set up by hand](#set-up-by-hand));
- otherwise it keeps running, its card says **Running since before the last update —
  look at what was found**, and what the panel found is folded under that line.

## Safe by default

A copy holds your real customers' data, so it starts quiet:

| Switch | Off (the default) | On |
|---|---|---|
| **Send email from the copy** | every outgoing email is written to the copy's log instead (Laravel's `log` mailer — and a 6amMart copy's own mail settings in its admin say off; a WordPress copy writes each email to its *WordPress debug log*) | the copy mails exactly as production does |
| **Run the queue worker and the scheduler** (6amMart) | nothing runs by itself against copied orders, payouts or subscriptions | both run, as on production |
| **Run the scheduled tasks** | production's scheduled tasks are copied and switched off (WordPress's WP-cron too) | they run as on production |

**Where the panel cannot switch email off.** How an app sends mail is its own code. The
panel switches it off for 6amMart, for WordPress and for a PHP app that takes its mail
settings from a Laravel-style `.env` — Laravel itself, or an app whose `.env` already
names its mailer (`MAIL_MAILER`, `MAIL_DRIVER` or `MAILER_DSN`). The files such an app
reads over its `.env` are switched off with it — `.env.local`, `.env.prod`,
`.env.prod.local` and the like, and the `.env.local.php` that `composer dump-env`
writes — and if one of them sets the mailer in a way the panel cannot rewrite, the copy
says its email is not switched off rather than promise it. A staging site of a custom
app — Node.js, Create with AI, any other PHP app — keeps the
mail settings it was copied with. The panel tells you three times:
the **Create a staging site** dialog words the switch for the kind of website you are
copying, the copy's job log says so in a line starting **IMPORTANT**, and the copy then
carries an **email not switched off** mark beside its *STAGING* badge and a line in the
banner on each of its pages. Change the mail settings on the copy before you test
anything that sends email. On WordPress, a plugin that sends through its own service
instead of WordPress's mail function is not covered either.

Always, whatever the switches: search engines are told not to index the copy (every
response carries `X-Robots-Tag: noindex, nofollow`), and nothing on it is served
from a cache, so what you test is what runs.

You can turn a scheduled task on later from the copy's **Scheduled tasks** page, and
the queue worker from its **Services** page.

### Payments, SMS and push notifications off (6amMart)

A shop's copy holds production's payment gateway keys and its customers' phone numbers,
so **Payments, SMS and push notifications off** is ON when you make one. In the copy's
own database — never production's — it:

- switches off every online payment gateway (Stripe, PayPal, Razorpay and the rest)
  and digital payment at checkout, so the copy cannot charge a card;
- switches off every SMS gateway (Twilio, Nexmo, 2Factor, MSG91…) and Firebase's login
  codes by SMS, so no customer gets a text from the copy;
- removes the Firebase push key, so the copy sends no push notification to anyone's
  phone.

**Cash on delivery and the wallet stay as they are**, so you can still place a test
order. The gateways are switched off, not erased: their keys are still in the copy's
admin. To test a card payment, enter that gateway's **test-mode keys** in the copy's
admin and switch it on there — never production's live keys, and never production's
Firebase file, which would reach your real customers' phones.

The create dialog lists exactly what it will switch off, read from production at that
moment, and the job's log names each change. Anything the shop's database does not
have (an older or a customised 6amMart) is skipped and named in the log, never an
error. **Refresh from production** switches them off again after it copies
production's database in.

You can turn it off for a copy — in the create dialog, or later in the copy's
**Copy settings** (in its banner). The panel warns you first: the copy then keeps
production's payment gateways, SMS gateways and push key, so a test order can charge a
real card. Turned off later, it takes effect at the copy's next Refresh; turned back
on, it is applied at once.

**Two more things are always done on a 6amMart copy**, and the create dialog lists them
with the rest:

- **Uploads kept on S3.** The copy is switched to its own local storage, and the S3 key
  and secret in its database are replaced by a value S3 refuses — so the copy can write
  to and delete from production's bucket nothing. On the untouched 6amMart code,
  pictures that production keeps on S3 do not show on the copy.
- **Google reCAPTCHA off**, so you can sign in to the copy on its new address.

Its first `.env` also sets the queue to the database unless it is `sync`, `database` or
`redis`, and broadcasting to the log unless it is `reverb`, `log` or `null`: the copy's
jobs and events must not go where production's do. A queue worker of a copy is never
started by another job (a certificate, **Domain & SSL**, a restore) while its switch is
off. The keys that stay production's — maps, AI, social login — are listed in the
dialog too.

**A 6amMart copy is never activated.** 6amMart registers its CodeCanyon licence to
the address it is activated from, so activating the copy's address would
move your licence away from production — whose admin panel would then lock itself
within a day. The panel refuses it on a copy. 6amMart re-checks its licence about
once a day and may then ask the copy for activation; refresh the copy from
production instead of activating it.

## Refresh from production

**Refresh from production** (on the copy's card, or in the banner on its pages)
replaces the copy's database and files with production's as they are now. Anything
you changed on the copy is lost. **Its address never changes** — a copy you gave your
own domain stays on it, and the app is pointed at that address again — and its
switches stay. Production is only read.

While it runs the copy is **not started**: its address says "This staging site is being
refreshed." It is started again when the new files have been searched and the app has
been asked — see [What is asked again](#what-is-asked-again). A staging site of a
custom app keeps the files that hold its own database
password, and is started again when the search is clean; if the search finds
production's password in the new files, it stays not started and its card lists them.

Such a copy of a **Create with AI** website is not refreshed while Build with AI is making a
change on it — the panel asks you to wait for that change to finish — and while the
refresh runs, Build with AI on the copy waits in turn: a new change, an Undo or
*Restore this version* is answered with "this staging site is being refreshed from
production" until it is over.

No staging site is refreshed while an **AI Coding** turn is working on it (or waiting
for your answer): the panel says so — stop the turn, or let it finish, then press
Refresh again. A refresh that was already waiting its turn when the coding turn began
stops at its first step for the same reason, before anything of the copy is replaced.

## Backups

The scheduled backups **leave staging sites out**: a copy is disposable, Refresh rebuilds
it from production, and it would otherwise put a second full copy of your customers'
data into every backup. The **Backups** page lists each copy under the schedule, so
this is never silent, with an **Include in scheduled backups** switch for any copy you
want kept — the same switch is in the create dialog and in the copy's **Copy
settings**. A backup you run yourself (**Run backup now**) still takes every copy.

A copy that is left out is not in those backups, so it is not restored from them —
after a restore, make a new copy.

## Delete a copy

**Delete staging site** removes the copy's database, files, account, services, PHP pool,
nginx configuration and certificate — the same delete as any project, with nothing
kept back: the copies of its database that an import kept first, and its PHP slow log,
go with it. Production's data and code are not touched. A 6amMart staging site was one more project on
the server while it existed, so each shop had a smaller share of the PHP workers and of
the cache; the delete ends by giving those back — no restart is needed
(see [More than one project](https://www.allsweb.com/sixpanel/docs/multiple-projects), "Sizing the server"). What happens to its address:

- **A temporary address** is deleted at AllsWeb, with its DNS records and their
  history. If AllsWeb cannot be reached at that moment, the panel keeps the request
  and tries again when it starts and every hour until AllsWeb confirms — a temporary
  address is never left pointing at this server. A copy removed some other way (a
  website's own **Delete**, a shop deleted from **Projects**) has its address
  released within the hour too.
- **Your own domain**: its Cloudflare record is removed — only a record that still
  points at this server, whether the panel made it or you did; one you have since
  pointed elsewhere is left alone, and so is a record in an account the panel was not
  given. A record at another DNS provider is yours to remove. A copy that has both a
  temporary address and your own domain gets both: the dialog says so before you
  confirm.

## Working on a branch and promoting

When production's code comes from a git repository — a website connected to git, or a
6amMart project whose admin app (and customer website) were installed from git — a test
copy can work on **a branch of its own**, and **Promote** moves what you tested there to
production.

Moving tested changes to production **needs git**. A project deployed by upload or from
files can have a staging site and everything else on this page; it has no **Promote**,
and its copy's page says so.

### Make the copy on a branch

The create dialog has **Work on its own branch**, ticked. Choose:

- **A new branch** — `staging` is offered (or `staging-<the copy's short name>` when the
  repository already has a `staging`). The panel makes it from **the commit production
  runs right now**, so the copy's files and its branch agree from the first minute.
- **A branch the repository already has** — the copy is made from production and then
  deployed from that branch.

The copy uses **production's own way of signing in** to the repository — its deploy key
or its access token. There is nothing new to add at your git host.

Making a new branch means *writing* to the repository, and the panel asks your git host
first, with a push that moves nothing. If the answer is no — a deploy key added without
**Allow write access**, a token that may only read — the dialog says so before anything
is made, with a link to the repository's settings. Two ways on: give the key or token
write access, or make the branch at the git host yourself and pick it as an existing one.
(With a read-only key the copy still deploys from its branch; **Commit and push** and the
panel's own merge are what need write access.)

### The copy follows its branch

- **Deploy the copy** (on its git card, or **Update** on the copy's own **Deploys**
  page) pulls the branch.
- A **push** to the branch deploys the copy by itself when production has automatic
  deploys on: **one webhook serves production and its copies**. A push to production's
  branch deploys production as always; a push to the copy's branch deploys the copy; a
  push to any other branch is ignored. There is no second webhook to add.

### Changes made on the server

If you — or a coding agent — changed files on the copy (Files, SSH), the git card lists
them: **changed**, **new**, **deleted**, against the commit the copy runs. **Commit and
push** shows the list once more, asks for a message, and pushes one commit to the copy's
branch. Never to production's branch, and never a forced push; if the git host refuses
it, you read what it answered.

**After Commit and push, deploy the copy once.** The commit puts your change in git, and
the copy's files are already that commit — but the copy has not *deployed* it: its build,
its deploy commands and its database migrations have not run for it. While the card says
**committed on the server, not deployed on the copy yet**, press **Deploy the copy**, test,
and then promote. **Promote** waits for that deploy, so a migration never runs on
production before it has run on the copy.

The same holds for a commit that reached the branch another way — a push from your own
computer, production's commits brought in — when the copy's deploy of it stopped
part-way (a migration that failed, say). The card then says **its last deploy there did
not finish**: fix what failed, press **Deploy the copy** until it finishes, test, and
then promote.

While a website's copy holds changes that are not in git yet, **Deploy the copy** on the
card is refused, and a push to its branch deploys nothing (the webhook's delivery says
why): the deploy would put the branch's files over them. **Commit and push** first.
(**Update** on the copy's own **Deploys** page stays the ordinary deploy: it replaces the
files with the branch's.)

A deploy that stopped half-way is not such a change. A PHP website's deploy puts the new
files in place before it installs packages and runs your deploy commands; when one of
those fails, the copy still runs the commit it ran, and the files that deploy brought are
not listed as changed. Put right what failed and press **Deploy the copy** again, or push
the fix. A file you edit on the server after that is listed as always, and **Commit and
push** commits it on top of the commit that deploy brought.

Left out on purpose, because they are the server's and not your code: every `.env` file,
`node_modules/`, `vendor/`, build folders (`.next/` and the like), upload and cache
folders at the top of the project (`storage/`, `uploads/`, `cache/`, `public/uploads/`,
`wp-content/uploads/`), log files, a `package-lock.json` or `composer.lock` the install
wrote because your repository has none (one your repository has is listed when it
changed), a coding agent's own files at the top of the code when your repository does
not have them (`.claude/`, `.codex/`, `.cursor/`, `.mcp.json`, `CLAUDE.md`, `AGENTS.md`),
anything your `.gitignore` leaves out, and what the
website's settings keep between deploys. On a 6amMart copy also what the app itself
owns: the activation result, the module switches and translations typed in the admin
panel. And two things of the copy itself, found by what they hold: every file that holds
**the staging site's own database password**, and what **arrived from production** when
the copy was made or refreshed (see
[The staging site's own database settings never travel](#the-staging-site-s-own-database-settings-never-travel)).

On a website's copy two more things are never listed and never committed. A folder that
is a git repository of its own (somebody ran `git clone` or `git init` on the server):
make it part of your project in your own repository if you want it there. And files your
repository marks `export-ignore` in `.gitattributes`: a deploy never puts them on the
server, so their absence is not a deletion and the commit leaves them in the branch.

A WordPress website's theme or plugin is changed in its repository, not on the server:
push to the branch and the copy deploys it.

### Production moved on

The card says how many commits the branch is **ahead** of production's branch and how
many it is **behind**. **Bring production's commits in** merges production's branch into
the copy's, pushes, and deploys the copy — so what you test includes what production
got meanwhile. If both changed the same lines, nothing is merged or pushed and the
files are named: settle them on your own computer, push, and the copy deploys the
result.

### Promote

**Promote to production** is offered when all five are true, and the card says which is
not:

1. the branch is **ahead** of production's — there is something to move;
2. it is **not behind** — production's newer commits were tested with it;
3. **nothing changed on the server is uncommitted**;
4. **the copy runs the branch's newest commit**;
5. **the copy has deployed it** — after **Commit and push**, or after a deploy of the
   copy that stopped part-way, that is one press of **Deploy the copy** that finishes.

Together they mean production receives **exactly the files the copy ran**, after the
copy's own deploy ran for them — with one exception that is on purpose: a file that
holds the staging site's own database settings stays on the copy, and production gets
the repository's version of it. You type production's short name, and one job runs on
the production project:

1. checks all five again;
2. **backs up production's database** — a dump kept beside the project; the newest two
   stay;
3. merges the branch into production's branch (a merge commit, with your message) and
   pushes it;
4. **deploys production** the ordinary way: pull, build, **database migrations**, caches;
5. checks that production answers.

**Files changed by hand on production** that are not in git are looked for before the
merge. The dialog lists them, and the promote is **refused** until you choose: tick
*throw these edits on production away* — the repository's version then replaces them —
or make the same change on the staging site, commit it there, and promote again. A
connected assistant can press Promote for you only with that list empty: the tick is
yours alone.

If anything after the backup fails, the panel **puts production back**: its database
from that dump — when a migration could have touched it — and its code to what it ran.
The job's last line then says the one thing it cannot undo: **the merge is still on
production's branch at your git host**. Fix the problem on the copy's branch and promote
again, or revert that merge at the git host; until then, an **Update** of production
would deploy it.

Putting the database back returns it to the moment of the backup: what visitors did in
the minutes between is lost with it. That is why it happens only when the deploy got as
far as changing the database.

### Open a pull request instead

**Open a pull request** on the card opens your git host's compare page for the two
branches (GitHub, GitLab, Bitbucket). Review and merge there. Once production's branch
holds what the copy's branch holds, the card says **Merged at the forge** and the button
becomes **Deploy production**: the same job without the merge — backup, deploy,
check, and put back on failure. This also works with a read-only key.

### Database changes travel as files

A database change reaches production the way code does: as a migration file in the
repository, run by the deploy — on the copy first, on production at promote.

- **6amMart**: every update runs `artisan migrate --force`.
- **Laravel app**: its **deploy commands** (Settings), for example
  `php artisan migrate --force`.

Nothing copies the copy's *database* to production. Rows you added while testing stay
on the copy — and so does a table or a column you changed **by hand** there: see
[Database changes made by hand](#database-changes-made-by-hand).

### Refresh and delete, on a branch

**Refresh from production** brings production's database and files again and then puts
the **branch's code back** on top, running its migrations — a rehearsal of the promote on
fresh data. It is refused while the copy holds changes that are in no commit, unless you
tick **throw the copy's changes away**. (A 6amMart copy only moves forward: when the
branch does not contain what production now runs, the refresh leaves production's code
and tells you to press **Bring production's commits in**.)

**Delete staging site** offers **also delete the branch** when the panel made it. A branch
you made yourself is never deleted.

## Without git: taking a tested change to production

A project that is not deployed from a git repository has no **Promote**. Its copy's card
says how a change you tested reaches production, for its kind:

- **6amMart** — *Tried an update here? Run the SAME zip on production:* its **Deploys**
  page → **Update from a zip**. It keeps `.env` and the uploads and runs the database
  migrations.
- **A static website or a Laravel app** — upload the **same ZIP** on production's
  **Deploys** page. **Never a ZIP made from this staging site's folder**: it holds the
  staging site's database settings, and production refuses it. An upload is a whole
  version: a file that is not in the ZIP is removed.
- **WordPress** — deploy the same ZIP of your theme or plugin on production's
  **Deploys** page. An update, or a setting changed inside WordPress itself, is made
  again on production.
- **Create with AI** (a staging site made on an earlier version) — ask Tia for the same
  change on production's **Build with AI** page.

## Database changes made by hand

A change you made by hand in the staging database — a new table, a changed column —
**does not travel**. Promote carries files, and nothing compares the two databases for
you while it runs. The by-hand road:

1. On the **staging site's Database** page press **Compare with production**. It reads
   both databases' structure once, changes nothing, and shows three lists — "only
   here", "only on production", "different definition" — for tables, columns and
   indexes, and the line "not compared: rows, views, triggers, routines, foreign keys".
2. **Write the change as SQL yourself** (`ALTER TABLE …`). The panel writes no SQL for
   you and runs none.
3. Run it on **production's Database** page with **Import**. **Every import takes a
   copy of the database first**: the panel checks there is room for it, keeps the newest
   two in a folder of their own, and its answer names the file. With no room the import
   is refused and nothing is changed.

There is no "structure only" export, on purpose: a file made the way the panel dumps a
database drops every table it names when it is imported. And never export a staging
site's whole database and import it on production — that puts test rows over real ones.

## What a staging site cannot promise

The limits, said plainly:

- **An app that hides its database password** — encrypted, split in two, or fetched
  from somewhere else — cannot be seen by any search. That is why the panel never
  starts a custom app's staging site by itself — the Staging & Testing page offers none,
  and one AI Coding sets up waits for your press — and why the four kinds it does start
  are **asked**: the search is the second check, never the only one.
- **A promote that fails after a migration ran** puts production's database back to the
  copy taken first: what visitors wrote in those minutes is lost with it.
- **A Laravel app's own payment, SMS and storage keys** stay as they were copied: the
  panel switches its email off and asks about its database, queue, cache and disk, and
  does not know what else your code calls. Change them on the copy before you test
  anything that uses them. And a static site's pages may call production's API from the
  staging address.
- **phpMyAdmin and root's own shell are by hand.** What you import through phpMyAdmin,
  or start as root with `systemctl`, the panel does not see.
- **If you point a staging site at production by hand** — its password, or its database
  name, typed into a file — nothing stops you at the keyboard. The next deploy, refresh
  or **Look again** finds it and stops the staging site; until then it runs with what
  you typed. Press **Look again** after you change such a file.

## Good to know

- A copy cannot be copied: make another copy of the original instead.
- **If you delete a shop that has staging sites**, the staging sites stay, each as a
  project of its own: it can still be updated, looked at and deleted, but no longer
  refreshed — there is nothing left to copy from. Its files are still searched for the
  deleted shop's database password for as long as that shop can be put back. One case
  has no way on: a customer website that was **not started** (its build named
  production's address) stays not started once the shop's data has been removed for
  good — delete that staging site when you no longer need it.
- A copy stays on the server it was made on, beside the project it is a copy of —
  **Move project** refuses it. Make a new copy on the other server.
- A **temporary address** is DNS-only (not through Cloudflare) and its certificate is
  Let's Encrypt's, issued over HTTP — port 80 must be reachable from the internet.
  For the same reason it still answers when **Refuse visitors that skip Cloudflare**
  is on: it never goes through Cloudflare. Its DNS record is AllsWeb's, so the panel
  never changes it — even with a Cloudflare token stored — it is not listed on the
  Cloudflare tab, and the check-up does not count it as one of your addresses.
- **Your own domain** on a copy is treated as any domain of yours, name by name: its
  record is proxied, it is on the Cloudflare tab and in the check-up, and with
  **Refuse visitors that skip Cloudflare** on it must be behind Cloudflare like every
  other address — a copy that has both kinds of address answers directly on the
  temporary one only.
- If this server's public IP address changes, the copies' temporary addresses are
  moved to the new one automatically.
- Each AllsWeb account can hold a limited number of temporary addresses at once;
  delete copies you no longer need.
- The copy's short name works everywhere a project's does — its own pages, the
  `sixpanel` command, a backup you start by hand.
