Skip to content
Sign in

Central administration

The central administration is a separate SvelteKit application for managing content on multiple WBengine websites. It connects to each site through the optional API v2; it is not installed as a PHP or Composer dependency.

This page describes what the central administration does and how a site is connected, then how to install and run it. The sections, in order:

  • Separate application, connected sites; what an editor can manage; site users and settings; what it does not replace
  • Connecting a site step by step
  • Installing and configuring: requirements, settings (environment variables), start with Docker Compose, HTTPS reverse proxy, first administrator
  • Accounts and access: signing in, roles, users and invitations, invitation by email
  • Operating the administration: master key and backups, command line, updating
  • Languages and translations

Separate application, connected sites

Each WBengine website keeps its own PHP application, MySQL database, files and public pages. The central administration runs separately with its own Node.js service and database. It stores its administrator accounts, site registry and encrypted integration credentials. It does not connect directly to a remote website database and it does not copy the site code into the central service.

The connection is server to server over HTTPS. A site operator enables API v2, provisions an integration key with only the required scopes and securely provides it to the central service. The central service validates the destination and TLS certificate, keeps the key on the server, and calls only supported API operations. Browser pages never receive the remote key. A site can revoke its key, and a failure or shutdown of the central service does not take the public website offline.

What an editor can manage

Depending on the site API capabilities and the central account role, an editor can work with articles, pages, menu entries, sections, registered boxes, translations and supported images across multiple sites. Article edits are saved as drafts. An editor can preview a private draft and publish it explicitly when ready. Existing revisions can be restored as a new draft.

The editor handles simultaneous changes safely. Every write includes the version it was based on. If another editor or local process changed the same content, the API reports a conflict instead of silently overwriting it. The editor preserves the typed text and lets the user review the current remote version before making a new explicit save.

Supported images are public raster media, not private draft attachments. Box choices come from a site-side allowlist. The central client cannot choose arbitrary PHP classes, methods, SQL statements, templates or filesystem paths.

Site users and settings

For a site administrator, the central administration also covers the accounts that sign in on the site itself and two server-side actions, when the site allows it.

  • Site users: list and search the accounts, create an account, change its role (user, editor or administrator), disable and enable it. A new account has no password: the administration shows a one-time link once, and the owner opens it on the login page of the site to choose a password. Passwords are never set, read or stored by the central administration, and the last active administrator of a site cannot be demoted or disabled.
  • Site settings: the effective settings of the site, read only, with secret values shown only as set, and a button that flushes the site cache. The settings themselves are changed on the server.

Both pages need an integration key issued with the matching permissions; a key without them asks to be replaced.

The Activity page lists the changes made on the site (who changed what and when) from its change log, read only. It needs the audit:read permission. The interface of the central administration is available in English, Czech, German and Polish, and each person chooses the language in their profile.

What it does not replace

The central service is an optional content client. It does not replace WBengine, the site database, source control, deployment pipeline or server administration. Developers still change PHP classes and templates in the site project and deploy them through the normal deployment workflow for that project. Sites continue serving pages independently if the central administration is unavailable.

The administration is distributed separately from the PHP framework. Its source is public on GitHub under the MIT license. It is not a Composer package, and no ready-made container image is published: the image is built from the source with Docker Compose. Installation, every setting and operation are described in the sections Installing and configuring, Accounts and access and Operating the administration below.

Connecting a site step by step

The central WBengine Admin is a separate application (for example at https://admin.example.org) that manages many sites through the API v2. A site is connected once; afterwards editors work in the central application.

Requirements on the site: apiV2.enabled set to true in Settings.json, HTTPS, the framework database updates applied (sh bin/update-database.sh, check with vendor/bin/wbengine.php migration:status) and an account with the role admin (see Users and the first login on the Getting started page).

  1. On the site, sign in as administrator and open /integrations/. If something is missing, the page lists what to fix before it issues a key.
  2. Choose the permission groups the key may use (Content, Publishing, Structure, Images, Site administration, History and optionally Analytics; the default grants all of them), confirm your password, keep the suggested name and issue the key. The key, the site address and the suggested name are shown once. The site stores only a hash of the key, so a lost key cannot be shown again: issue a new one.
  3. In the central Admin, sign in with your password and the one-time code from your authenticator app. Every central account uses multi-factor authentication. The first global admin is created on the Admin server with npm run admin -- invite EMAIL global; later a global admin invites people on the Users page, optionally by email (see Users and invitations below), and the person activates the account at /accept.
  4. Open Sites, choose Add site and enter the name, the HTTPS address and the key. Before anything is saved, the Admin verifies the key against the site (capabilities and identity, and that the key allows at least one permission group). The key is then stored encrypted and is never sent to the browser again.
  5. The site appears in the list at once. To rotate a key, issue a new one on the site, paste it under Replace integration key in the Admin and only then revoke the old key on the site's /integrations/ page.

A key can also be bound to the addresses it may be called from. The key form has an optional field for IP addresses or ranges, and after the first use the card of the key shows the address it was used from, with a form to restrict the key to it. A call from another address is refused. Behind a reverse proxy the site must have http.trustedProxies set so that it sees the real address of the caller.

An AI assistant that supports the Model Context Protocol, for example Claude Code, can write and publish the articles of a site. The WBengine MCP server is the single file wbengine-mcp.phar from the releases of the framework. It runs on the machine of the assistant and calls API v2 of the site with an integration key. The key form offers presets for such a key (Editor with structure and statistics, Agent for content, Agent drafts only, Read only), and after the key is issued the page shows the client configuration with the key once. The editor preset also lets the assistant manage pages, the menu, boxes and images and read statistics, and the assistant explains how the site works to an editor who is not technical. Articles are saved as drafts and published only on request, and a published article can be hidden again. Text read from the site is passed to the assistant as data, not as instructions.

A site that does not have the integrations page yet gets it with one command, run in the site directory: vendor/bin/wbengine.php integrations:install --config=src/Config/Settings.json. Without further options it only lists what it would create. With --apply it creates the page /integrations/ (for signed-in users only), the section and the box record. No class in the application is needed: the box ships with the framework. Existing records are never changed, so the command can be run again. It also lists what still prevents issuing a key, for example API v2 being disabled or missing database updates.

Typical problems and their meaning:

  • The key is not valid or was revoked: issue a new key on the site.
  • The key does not allow anything the Admin can manage: issue a new key with at least one permission group. A page the key does not cover asks for a new key with the missing group.
  • API v2 is not available: enable apiV2 and apply the framework database updates, then reload.
  • Too many failed attempts: the site limits failures per address, wait a few minutes.
  • The site is already registered: the address is unique in the central Admin.

Installing and configuring

This part is about running the administration itself: a SvelteKit application with its own SQLite database (one file, no database server), started with Docker Compose from the source on GitHub.

Requirements

  • Docker with Docker Compose. The repository contains the Dockerfile (Node 24) and compose.yaml, which starts the application with one volume for its data.
  • A host name for the administration and an HTTPS reverse proxy in front of it (see below).
  • Node.js 22.18 or newer only when you run the tests or the command line outside Docker.

Settings (environment variables)

All settings are environment variables. Copy .env.example to .env next to compose.yaml and fill it in. The file contains secrets: keep it out of Git and readable only by the user who deploys (on a server, mode 0600). Compose passes the variables to the application.

Variable Required Meaning
DATABASE_PATH set by Compose The path of the SQLite file, /app/data/admin.sqlite in the admin-data volume. There is no database server and no database password to set.
MASTER_KEY yes 32 random bytes as 64 hexadecimal characters, for example from openssl rand -hex 32. It encrypts the integration keys of the sites and the secrets of two-step verification in the database.
ORIGIN yes The exact HTTPS address of the administration without a trailing slash, for example https://admin.example.com. The application compares requests with it, and the link in an invitation email is built from it.
REMOTE_IP_ALLOWLIST no, empty Exact IP addresses, separated by commas, of sites on a private network that the operator has approved. No ranges and no host names. Without it only sites on public addresses can be registered.
MAIL_SMTP_HOST no SMTP server for invitation emails, for Google smtp.gmail.com.
MAIL_SMTP_PORT no, 587 587 (STARTTLS) or 465 (TLS from the start).
MAIL_SMTP_USER no The account used to sign in to the SMTP server, for Google the full address.
MAIL_SMTP_PASSWORD no Its password, for Google an app password.
MAIL_FROM no The sender address of the invitation email.

Compose also sets the listening address and port inside the container and the limit of a request body (1 MiB); there is nothing to configure. The MAIL_* variables work together: see Invitation by email below.

Start with Docker Compose

git clone https://github.com/rrsoftcz/wbengine-admin.git
cd wbengine-admin
cp .env.example .env        # fill in MASTER_KEY and ORIGIN
docker compose run --rm app npm run admin -- migrate
docker compose up -d --build app

The application does not change the database on its own. The migrate command applies every file of migrations/ once and records it, so run it before you start a version that contains a new migration. The application listens on 127.0.0.1:13000.

HTTPS reverse proxy

The application assumes that a reverse proxy (Nginx, Caddy or similar) terminates HTTPS and forwards to 127.0.0.1:13000. ORIGIN must be exactly the address the visitors use. The proxy must pass the correct Host header and must remove or overwrite the forwarded headers it receives, because the application does not trust proxy headers. Cookies are always Secure, in development too, so even a local run needs an HTTPS proxy in front of it.

First administrator

There is no public registration and no default account. The first global admin is created on the server with the command line:

docker compose run --rm app npm run admin -- invite you@example.com global

The command prints a one-time code valid for 48 hours. Open /accept of the administration, enter the code and choose a password (14 to 256 characters). The next step is the setup of two-step verification.

Accounts and access

Who can sign in, what each role may do and how people are invited.

Signing in

  • A password and a code from an authenticator app (TOTP, 6 digits, 30 seconds). Two-step verification is mandatory for every account.
  • Ten backup codes are shown once when the verification is set up. A backup code replaces the code from the phone when the phone is lost. Verification cannot be switched off on the web, and a password alone never replaces it.
  • Failed attempts are limited: 8 password attempts and 12 verification attempts per 15 minutes.
  • A session lasts 8 hours.
  • A forgotten password is recovered with a one-time code valid for 30 minutes that a global admin issues for the account (the Users page or the command recover). A new password signs the account out everywhere and keeps the verification.

Roles

A person has a role per site. Every operation also needs a matching permission of the site's integration key and a capability of the site.

Role What the person can do
Editor Read the site, edit article drafts and their translations, upload images.
Site admin Everything an editor can, and change the structure (pages, menu, sections, boxes), publish and remove content, and see the site users, the settings and the Activity page of the site.
Global admin Everything above on all sites, and manage the registry of sites (add a site, replace a key) and the accounts of the administration.

Users and invitations

A global admin manages the accounts at /users: search, invite, disable and enable, promote and demote global admins, issue a recovery code, reset two-step verification and revoke access to a site. Nobody can change their own role or status there, and the last global admin is kept.

To invite someone, enter the email, choose the level (team member or global admin) and, for a team member, optionally the first site and the role on it (editor or site admin). The administration shows a one-time code valid for 48 hours, once. Hand it over through a secure channel. The person opens /accept, enters the code, chooses a password and sets up the verification. An email that already has an account cannot be invited again.

Invitation by email

By default the code is handed over by hand. When an SMTP server is configured, the invite form gets a second button, Create and send by email. It sends the person a welcome text, the link to the invitation page (built from ORIGIN), the code and the information that the code is valid once for 48 hours. The code is still shown on the screen, so you can always pass it on yourself. The email is written in the interface language of the administrator who sends it (English, Czech, German or Polish). Without the configuration the button is not shown and nothing else changes.

Set MAIL_SMTP_HOST, MAIL_SMTP_USER, MAIL_SMTP_PASSWORD and MAIL_FROM together; MAIL_SMTP_PORT is 587 unless you set it. If some of them are missing, the feature stays off and the administration logs one warning at start. The connection is always encrypted and the certificate of the server is always verified.

With Google (a Google Workspace account on your own domain is preferable, a personal Google account works too):

  1. Turn on two-step verification for the sending account.
  2. Create an app password for it and use that, never the password of the account. Google can change its rules for app passwords and limits, so check its current documentation.
  3. Put the values into .env on the server: MAIL_SMTP_HOST=smtp.gmail.com, MAIL_SMTP_PORT=587, MAIL_SMTP_USER the address of the account, MAIL_SMTP_PASSWORD the app password and MAIL_FROM the address of the account or an alias verified in Google as Send mail as (Google replaces any other sender).
  4. Restart the application: docker compose up -d --build app.

"Sent" means that the SMTP server accepted the message. It does not promise that the email reaches the inbox: that depends on the mail setup of the sender domain (SPF, DKIM and DMARC). If sending fails, the page says why: the mail server rejected the login, the address was rejected, there is no connection to the mail server, the mail server did not answer in time, or an unknown error (the details are in the server log). The invitation stays valid in every case, so the code can be handed over by hand. The password of the mail account is never written to the log or shown. Both the sent and the failed attempt are recorded in the audit.

Operating the administration

Secrets, backups, the command line and updates.

Master key and backups

The database of the administration is one SQLite file, admin.sqlite, in the admin-data volume of the container. It holds the accounts, the registry of sites, the permissions and the audit. Integration keys of the sites and the secrets of two-step verification are stored encrypted with MASTER_KEY.

  • Keep MASTER_KEY in a secret store outside the database and outside the backups of the database. A restored database is readable only with the same key.
  • Changing the key is a controlled re-encryption of the stored data. The administration has no automatic rotation.
  • Back up the database regularly with docker compose run --rm app npm run admin -- backup /app/data/backups/admin.sqlite. It writes a consistent copy while the application keeps running; only its owner can read the copy. The copy holds encrypted keys and password hashes, so never store it together with MASTER_KEY. To restore, stop the application, put the copy in place of admin.sqlite in the admin-data volume and start it again.
  • Run exactly one instance of the application. The database file belongs on a local volume, not on a network share.
  • If a site's key is ever lost or exposed, issue a new one on the site, enter it under Replace integration key and only then revoke the old one on the site.

Command line

The command line works directly on the server with the environment of the application. Access to it is privileged. It never sends email.

docker compose run --rm app npm run admin -- migrate
docker compose run --rm app npm run admin -- invite EMAIL global
docker compose run --rm app npm run admin -- invite EMAIL member SITE_ID editor
docker compose run --rm app npm run admin -- recover EMAIL
docker compose run --rm app npm run admin -- users
docker compose run --rm app npm run admin -- revoke USER_ID SITE_ID
docker compose run --rm app npm run admin -- backup /app/data/backups/admin.sqlite
printf '%s' "$KEY" | docker compose run --rm -T app npm run admin -- site-add 'Site name' https://example.com/

migrate applies the database migrations. invite and recover print a one-time code (48 hours and 30 minutes). users lists the accounts. revoke removes the permission of an account for a site. backup writes a consistent copy of the database (see above). site-add registers a site; the integration key is read from the standard input so that it never appears in the command line or the shell history. The usual way to add a site is the Sites page of the administration.

Updating

Fetch the new version (git pull), run migrate as above, then docker compose up -d --build app.

Languages and translations

A WBengine site can serve its content in several languages. The languages are site configuration, not content: they are defined in the i18n section of src/Config/Settings.json and nowhere else (not in the database and not through the API). The central Admin only reads them.

"i18n": {
    "enabled": true,
    "default": "en",
    "locales": ["en", "cs"],
    "detect": ["url"]
}
  • default is the language of the original content. Articles, pages and menu items are stored in this language in their normal tables.
  • locales lists every language the site serves, including the default one. Use short codes such as en, cs or pt_BR.
  • detect says how the language of a request is chosen: url (a prefix such as /cs/blog/), cookie (the last choice) and header (the browser language), in this order of preference.

How translations are stored

A translation is not a new article and not a new URL. It is one more row for the same article, page or menu item, identified by the record ID and the language code (tables cms_article_translations, cms_site_translations and cms_menu_translations). A field that is empty in the translation falls back to the default language, so a half translated record is still complete.

The default language is served without a prefix (/blog/) and every other language with its code as a prefix (/cs/blog/). Both versions use the same page and the same article.

Add a language

  1. Add the code to locales in Settings.json and reload the application. The central Admin offers the new language after its next connection to the site.
  2. If the templates and buttons of the site use translated interface texts, add a language file to the Locale directory (see the dir option of i18n). Without it the interface texts stay in the default language.
  3. Translate the content in the central Admin.

Change the default language

Edit default in Settings.json. Keep in mind that all existing content is stored in the old default language, so after the change the same texts would be treated as the new language. Translate or copy the content first, then change default.

Translate content in the central Admin

The lists of articles, pages and menu items have a Languages column such as "3 of 20 languages"; expanding it shows which languages are missing. In a record, the Content language select above the form groups the languages as original, translated and missing. Choosing a language loads its content into the same form. The last five changes appear in the right panel; the full history with restore opens through All changes and restore. A translation is saved to the same record. Translations of articles are saved to the draft first and go live together with the article when you publish it.