Skip to content

Configuration

newmeta strictly separates configuration into versioned defaults (config.php) and local secrets (config.local.php). On top of that, the Nuxt theme reads a .env, and CORS, API keys, and 2FA are managed dynamically in the backend.

config.php vs. config.local.php

FileContentsGit
config.phpsystemVersion, directory constants, password salt, base URL derivationversioned
config.local.phpDB credentials, update-server credentialsgit-ignored
config.local.example.phpTemplate for new installsversioned

config.php loads config.local.php at the top:

php
// config.php (excerpt)
if (file_exists(__DIR__ . '/config.local.php')) {
    require_once __DIR__ . '/config.local.php';
}

$GLOBALS['systemVersion'] = "3.0.0";
$GLOBALS['pw_salt']       = '...';
$GLOBALS['baseurl']       = 'https://' . $_SERVER['HTTP_HOST'];

Never change pw_salt

$GLOBALS['pw_salt'] is part of every user password hash. If you change the salt after the fact, every login fails. On a new install, set a fresh salt once — then leave it alone.

baseurl is hardcoded https://

$GLOBALS['baseurl'] does not check $_SERVER['HTTPS'] — the scheme is fixed to https://. Local development over http:// still works, but features that use baseurl to build absolute links (emails, sitemap, Schema.org) will emit wrong URLs. For local work: either use an HTTPS vhost (e.g. mkcert) or override $GLOBALS['baseurl'] manually.

config.local.php

Minimal template (config.local.example.php):

php
<?php
$GLOBALS["db_host"] = "localhost";
$GLOBALS["db_user"] = "";
$GLOBALS["db_pw"]   = "";
$GLOBALS["db_db"]   = "";

$GLOBALS["update_user"]     = "";
$GLOBALS["update_password"] = "";
$GLOBALS["update_base_url"] = "https://update.newmeta.de";

Update-server credentials

The Update Manager under /admin/update pulls ZIP updates from the update server. Without credentials, Git-based workflows still work — only in-place updates are disabled.

php
$GLOBALS['update_user']     = 'username';
$GLOBALS['update_password'] = 'password';
$GLOBALS['update_base_url'] = 'https://update.newmeta.de';

See the Update Manager section in the README; dedicated update docs are coming later.

Theme .env files

The Nuxt theme lives under _theme/vue-base/ and reads a .env on dev and build:

env
# _theme/vue-base/.env
NUXT_API_KEY=123456
NUXT_PUBLIC_API_BASE=http://newmeta.local/api
NUXT_PUBLIC_SITE_URL=http://newmeta.local
NUXT_PUBLIC_THEME=vue-base

In production, set the API and site URLs to the public domain (e.g. https://www.example.com/api / https://www.example.com).

VariablePurpose
NUXT_API_KEYInternal key for server-to-server calls from Nuxt (e.g. SSR fetches)
NUXT_PUBLIC_API_BASEREST API base URL, exposed to the client
NUXT_PUBLIC_SITE_URLCanonical site URL (Open Graph, sitemap, Schema.org)
NUXT_PUBLIC_THEMEActive theme name — controls alias resolution (see Themes › Nuxt aliases)

PurgeCSS safelist

Project-specific classes can be added without forking nuxt.config.ts:

env
NUXT_PURGECSS_SAFELIST_STANDARD=ma-,foo-    # exact prefix match
NUXT_PURGECSS_SAFELIST_GREEDY=bar-          # contains match
NUXT_PURGECSS_SAFELIST_DEEP=baz-            # deep selector match

Each prefix becomes a ^prefix regex and is merged with the defaults (^ns-, ^swiper, ^fa-, …).

CORS / allowed origins

CORS is dynamic — your own domain is always allowed, and additional origins are managed in the backend:

  1. Admin → /admin/api-keysAllowed Origins tab
  2. Add an origin (e.g. http://localhost:3000 for a Nuxt dev server against a remote backend)
  3. Activate — no deployment step needed

Requests from unregistered origins are rejected globally — that includes public endpoints like /api/pages. The exact failure mode (CORS reject without body vs. HTTP status code) depends on where in the request the rejection happens.

Local without cross-origin

If you serve the Nuxt theme through the same Apache vhost as the API, you have no CORS problem. The dev server (localhost:3000) against a remote API host, on the other hand, always needs an origin entry.

API keys for external access

External tools authenticate via the X-Api-Key header:

bash
curl -H "X-Api-Key: nscms_k1_your_key_here" https://your-domain.com/api/pages?view=start

Managed under /admin/api-keys — each key has configurable endpoints, HTTP methods, expiry, and allowed origins. Keys are stored as SHA-256 hashes and shown only once on creation.

Webhook subscriptions, HMAC secrets, and delivery history live in the same panel — see Plugins › Webhook events.

2FA setup for admin logins

2FA (TOTP per RFC 6238) is mandatory for every backend login:

  1. First login → scan the QR code (Google Authenticator, 1Password, Bitwarden, Authy, Microsoft Authenticator, …)
  2. Enter the 6-digit code
  3. Save the eight recovery codes — shown only once
  4. Every subsequent login additionally requires the TOTP code

The code comparison is timing-safe via hash_equals() — see _core/system/auth/TotpAuth.php::verifyCode(). The pending-2FA session expires after 5 minutes (auth/model.php::verify2fa()).

Brute-force protection: 2FA verification shares the login-attempts counter (loginattempts table, IP-based, 15-attempt limit) with the password check. A successful login clears the counter.

Keep your recovery codes

If an admin loses the second factor and the recovery codes, only a manual DB edit in user_recovery_codes can recover access. There is deliberately no web-UI reset.

Rate limiting

Rate limits are enforced via APCu — token bucket per identifier (backend user ID, API key, or IP), no DB load. The limiter is the first check in the request lifecycle and shields the database under load.

  • Global limits: frontend 100 req/min, backend 250 req/min, API key 60 req/min
  • Endpoint-specific: stricter per-IP limits on sensitive endpoints (auth 10 attempts, checkout 10/min, elearning/certificate 5/min)
  • Configuration: _core/system/api/Ratelimiter.php ($endpointLimits array)
  • Standard headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After (on 429)
  • Graceful degradation: without APCu, the limiter silently returns allowed:true — requests are not blocked

Response cache

Anonymous GET responses are cached in APCu (X-Cache: HIT|MISS header). Configuration lives in the cache_settings backend plugin — enable/disable caching, set the TTL, and clear the cache from there.

Personalized requests (logged-in users via $_SESSION['userid'], backend users via $_SESSION['backend_loggedin'], API-key requests via HTTP_X_API_KEY) are never cached — that's hardcoded. In addition, endpoint-level exclusions live in _public/extensions/core/backend/cache_settings/config/cache_exclude_endpoints.json (prefix match). All backend/* endpoints are excluded from the cache globally.

See also