Kwisple
Browse the docs

Your site repository › Going live

Staging

Give a custom-code site a staging environment with its own content, address and password, deploy the staging branch behind the SDK's gate, and move code to production with "Naar productie".

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).

ProductionStaging
Site on the platformThe websiteA site with environment = staging, linked to production
ContentIts ownIts own (optionally a one-time copy to start from)
Content modelPushed by main's CISame model at creation; the staging branch's CI pushes its own
Token, webhook secretIts ownIts own
DeploymentProduction branchStaging branch, with KWISPLE_ENV=staging
Indexing, statistics, 404 logOnOff
Form submissionsProduction inboxStaging 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.

  1. 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.)
  2. Fill in Adres van staging (staging address), for example https://staging.acme.be. It is pre-filled as https://staging. plus the primary domain (or the Site-URL's host) without www.
  3. 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.
  4. 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_SECRET

At the host, give the staging deployment those KWISPLE_* values plus:

KWISPLE_ENV=staging

KWISPLE_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):

  1. Deploy the staging branch a second time, next to production.
  2. Give that deployment the KWISPLE_* variables of the staging site (kwisple link → choose the staging), plus KWISPLE_ENV=staging.
  3. Point the staging address at that deployment (DNS van staging, below).
  4. Next.js: stagingGate in proxy.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

RequestAnswer
/api/revalidate, /api/preview, and paths in openPass (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 nextThe 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 else401 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

FeatureOn staging
robots.txtDisallows everything.
sitemap.xmlEmpty.
<Analytics> (Next and Astro)Renders nothing.
404 reports, AI-agent reportsNot sent.
Forms (submitForm)Land in the staging site's own inbox, also while it is not live.
Live preview, revalidationWork 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).

  1. 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.
  2. Click Naar productie, then the confirmation Zeker? Live zetten op … with the production branch.
  3. 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:

  1. Create the staging branch and push it.
  2. On the server, check it out as a worktree next to the production checkout: git worktree add ../kwisple-<name>-staging staging.
  3. Write .env.staging in the production checkout: the staging site's own KWISPLE_TOKEN and KWISPLE_WEBHOOK_SECRET (from Instellingen → Ontwikkeling of the staging), the other KWISPLE_* values, and KWISPLE_ENV=staging. The compose service reads it at runtime and as the build secret.
  4. Set the staging host for Caddy in the platform's .env.production (for RANSON.N RANSON_N_STAGING_HOST=staging.ranson-n.be; without it Caddy serves no staging host). Caddy adds X-Robots-Tag: noindex, nofollow to every staging response as a second line of defence.
  5. Point DNS of the staging hostname at the server (DNS only).
  6. Build and start it: tools/go-live/deploy.sh site <name> staging (compose profile staging; it pulls the worktree, builds site-<name>-staging and 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.

Last updated 2026-10-30