A custom-code site can have one staging environment: a second site on the platform with its own content, its own token and webhook secret, and its own deployment of the repository's staging branch on its own address (https://staging.acme.be), behind a password and never indexed. The dashboard shows it as an environment of the website, not as a website of its own. Clients read Testversie (test version) everywhere; "staging" is the team's word.
Nothing is copied back and forth. What you publish on staging stays on staging; production keeps its own content. Only code moves, from the staging branch to the production branch, with Naar productie (to production).
| Production | Staging | |
|---|---|---|
| Site on the platform | The website | A site with environment = staging, linked to production |
| Content | Its own | Its own (optionally a one-time copy to start from) |
| Content model | Pushed by main's CI | Same model at creation; the staging branch's CI pushes its own |
| Token, webhook secret | Its own | Its own |
| Deployment | Production branch | Staging branch, with KWISPLE_ENV=staging |
| Indexing, statistics, 404 log | On | Off |
| Form submissions | Production inbox | Staging inbox |
Staging environments never count as websites: not in the site limit, apps, campaigns, the monthly Websitecheck or the client's main address. kwisple link lists them with "(staging)".
Creating it
Who can do this: the agency team, with the deploy right (site.deploy), for custom-code sites only.
- Open the production website and go to Instellingen → Staging. (Or, in the website list, open the card's Meer acties menu (the three dots) and choose Staging aanmaken.)
- Fill in Adres van staging (staging address), for example
https://staging.acme.be. It is pre-filled ashttps://staging.plus the primary domain (or the Site-URL's host) withoutwww. - Leave Begin met een kopie van de live inhoud (start with a copy of the live content) on to start from production's pages, or switch it off for an empty staging.
- Click Staging aanmaken (create staging).
What creation does:
- creates the staging site with production's name and settings, mode custom code, not live, and a slug
<production slug>-staging; - sets its Site-URL to the address, Webhook bij publiceren to
<address>/api/revalidate, Preview-URL to<address>/api/preview, and generates a new Webhook-geheim; - pushes production's current content model to it;
- with the copy on: seeds production's live content once, as a site kit. That is the live entries in the default language plus the globals, with enabled blocks. Like
kwisple export, a kit leaves out uploaded files and links to other entries (asset fields, entry relations, and link fields that point at an entry or an asset); re-add those on staging where you need them.
Not created: a token, domains, a hosting connection, a password. You add those next. A staging that waits in the trash blocks a new one ("Er staat nog een staging van deze website in de prullenbak…").
Linking the staging deployment
The staging deployment is the same repository, its staging branch, deployed a second time. It needs the staging site's own variables:
git switch -c staging
npx kwisple link # choose the website's "(staging)" entry: its own KWISPLE_TOKEN and KWISPLE_WEBHOOK_SECRETAt the host, give the staging deployment those KWISPLE_* values plus:
KWISPLE_ENV=stagingKWISPLE_ENV=staging must be set at build time too: robots and sitemap may be generated during the build. The settings page lists the same steps under Zo zet je staging op (how to set up staging):
- Deploy the staging branch a second time, next to production.
- Give that deployment the
KWISPLE_*variables of the staging site (kwisple link→ choose the staging), plusKWISPLE_ENV=staging. - Point the staging address at that deployment (DNS van staging, below).
- Next.js:
stagingGateinproxy.ts. Astro: the integration does it.
The gate
With KWISPLE_ENV=staging, isStaging() returns true and the SDK keeps the deployment private and out of search engines.
Next.js: stagingGate in proxy.ts
// src/proxy.ts (as in RANSON.N)
import { maybeReportAiAgent, stagingGate } from "@kwisple/sdk";
import { type NextRequest, NextResponse } from "next/server";
import { client } from "@/lib/cms";
export async function proxy(req: NextRequest) {
maybeReportAiAgent(client, req);
const gate = await stagingGate(req, {
client,
secret: process.env.KWISPLE_WEBHOOK_SECRET,
open: ["/beheer", "/api/beheer"], // paths with their own access check
clientIp: req.headers.get("x-forwarded-for")?.split(",")[0]?.trim(), // per-visitor limit on attempts
});
if (gate && "response" in gate) return gate.response;
return gate ? NextResponse.next({ headers: gate.headers }) : undefined;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.*\\.[a-z0-9]+$).*)"],
};stagingGate(request, { client, secret, open?, clientIp? }) returns null outside staging, { response } when it answers itself (the password page, a redirect after logging in), or { headers } to add to the response when the request may pass. Unlike the starter, this matcher includes /api/, so the gate covers API routes on staging. RANSON.N reads the IP with its own visitorIp() helper.
Astro
The integration's middleware runs the gate on every request when KWISPLE_ENV=staging, with the visitor's address from Astro, and adds the headers to every response. You write nothing. The middleware does not take extra open paths.
What the gate does
| Request | Answer |
|---|---|
/api/revalidate, /api/preview, and paths in open | Pass (they have their own signatures or checks). |
?kwisple-ticket=… with a valid ticket (the dashboard's Bekijk staging) | 303 to the same URL without the ticket, setting the session cookie. Tickets last ten minutes. |
POST /_kwisple/staging (STAGING_LOGIN_PATH) with password and next | The platform checks the password (POST /api/v1/staging/login, rate-limited per visitor). Right: 303 to next with the session cookie. Wrong: the page again with "Dat wachtwoord klopt niet." |
A valid session cookie (kwisple_staging) | Pass. |
| Anything else | 401 with a neutral password page in the site's default language (Dutch, French or English), titled "Testversie" followed by the site name, without any platform name. Without a password set, it says "Open deze testversie via het dashboard." |
The session is a signed cookie (HttpOnly, SameSite=Lax, Secure on https) for 14 days. It is signed with a key derived from the site's webhook secret and carries the password's version, so changing the password ends every older session at once. The gate asks the platform about the password state at most every 30 seconds.
Every passing response gets X-Robots-Tag: noindex, nofollow, Cache-Control: private, no-store and Vary: cookie. A staging deployment that runs with a production token stays closed: the platform only describes staging for staging sites.
Other staging behaviour
| Feature | On staging |
|---|---|
robots.txt | Disallows everything. |
sitemap.xml | Empty. |
<Analytics> (Next and Astro) | Renders nothing. |
| 404 reports, AI-agent reports | Not sent. |
Forms (submitForm) | Land in the staging site's own inbox, also while it is not live. |
| Live preview, revalidation | Work as in production, against the staging site's settings. |
Settings of the staging environment
Open the staging card and go to Instellingen → Staging.
Address and branches
Adres en branches has Adres van staging, Staging-branch, Productie-branch and GitHub-repository (owner/name), saved with Opslaan. The two branches must differ. When an environment has a hosting connection, its branch and repository come from there (read-only, marked "Komt uit de hosting-koppeling (Ontwikkeling)."). Changing the address also updates the staging site's Site-URL, Webhook bij publiceren and Preview-URL.
Password
Wachtwoord: type a Nieuw wachtwoord (minstens 8 tekens) and click Wachtwoord instellen (or Wachtwoord vervangen). The platform stores it hashed (scrypt). Wachtwoord weghalen removes it: staging then opens only through the dashboard. Password changes are recorded in the agency's logboek.
Naar productie
Naar productie merges the staging branch into the production branch on GitHub. It needs the repository and a GitHub-token: a fine-grained token for this repository only, with Contents: read and write, saved with Token bewaren (stored encrypted, never shown again).
- The panel lists the commits staging has that production lacks ("… wijzigingen op staging die nog niet op … staan", with the production branch), and warns when production has commits staging never showed.
- Click Naar productie, then the confirmation Zeker? Live zetten op … with the production branch.
- The merge commit message is
Naar productie: <branch> (door <name>). Hosts that build from the repository deploy production from that push; a production site on a deploy hook gets its build started.
A conflict is reported and never forced: resolve the merge in git, then try again. Content does not move: what was approved on staging is entered on production too. Each promotion is in the agency's logboek.
Opening staging
Bekijk staging (team) and Bekijk de testversie (client), on the website overview of both environments, open staging with a ten-minute ticket, so nobody needs the password.
DNS van staging
DNS van staging shows the records for the staging address when the live site runs on the platform's own server: A and AAAA records to the server, like production. When production runs at a host of its own (Cloudflare, Vercel, Netlify), the staging deployment gets its own address there; point the staging hostname at it as that host describes, or use the host's address as Adres van staging. Until production has a choice under Domein, the panel says so ("Kies bij de live website onder Instellingen → Domein waar de website draait…"). On Cloudflare DNS, keep the record DNS only (grey cloud). See Domains and DNS.
On the platform's own server
When production runs as a container on the platform's server (Hosting and deploys), staging is a second service next to it:
- Create the
stagingbranch and push it. - On the server, check it out as a worktree next to the production checkout:
git worktree add ../kwisple-<name>-staging staging. - Write
.env.stagingin the production checkout: the staging site's ownKWISPLE_TOKENandKWISPLE_WEBHOOK_SECRET(from Instellingen → Ontwikkeling of the staging), the otherKWISPLE_*values, andKWISPLE_ENV=staging. The compose service reads it at runtime and as the build secret. - Set the staging host for Caddy in the platform's
.env.production(for RANSON.NRANSON_N_STAGING_HOST=staging.ranson-n.be; without it Caddy serves no staging host). Caddy addsX-Robots-Tag: noindex, nofollowto every staging response as a second line of defence. - Point DNS of the staging hostname at the server (DNS only).
- Build and start it:
tools/go-live/deploy.sh site <name> staging(compose profilestaging; it pulls the worktree, buildssite-<name>-stagingand restarts it).
Removing it
Staging weghalen (remove staging), confirmed with Zeker? Staging naar de prullenbak, moves the environment and its content to the trash for 30 days. The deployment itself you remove at the host.