Deploy & operate

Operations and security

Since v1.2 the base ships these operational features. All are optional: leave the variables blank and the feature switches off, so existing sites are unaffected.

Scheduled publishing

Posts, pages and products can be scheduled (enter a future publish time in the editor and click "Schedule"). They go live on time and emit content.published: through /api/cron/dispatch when the dispatcher runs, and otherwise on ordinary traffic (a background check at most every 5 minutes per instance that never slows a request). Without cron, an item goes live with the first visit after its time.

Scheduled task dispatcher

Tasks configured in Admin → Scheduled tasks run on their cron expressions through POST /api/cron/dispatch.

NUXT_CRON_SECRET=<long random string>   # bearer token for the dispatcher
NUXT_CRON_TZ_OFFSET=480                 # schedule timezone in minutes (default China Standard Time)

Who wakes the dispatcher:

DeploymentHow
Cloudflare Pagesdeploy deploy/cron-worker (copy wrangler.toml.example, wrangler secret put CRON_SECRET, wrangler deploy); every 5 minutes by default
Linux / Baotacrontab or Baota scheduled task: curl -fsS -X POST -H "Authorization: Bearer $NUXT_CRON_SECRET" https://site/api/cron/dispatch
Many sites from one server + failure alertsone Baota "Shell script" task per site (every 15 minutes) running deploy/cron-baota/run.sh; on failure it prints NUXTCMS_CRON_ALERT and Baota's keyword alert sends it (Feishu / e-mail). See deploy/cron-baota/README.md
Manual catch-upsame command with ?all=1 runs every enabled task once

Each tick also does maintenance: task logs keep the newest 500 rows and expired rate-limit rows are purged.

Only admins can view, create and run scheduled tasks. The cron secret only goes to the site itself: tasks whose path starts with /api/cron/ always call the site's current origin (with Authorization: Bearer … added), and {{cronSecret}} in headers or bodies is filled in only for the site's own origin; anywhere else it becomes empty.

Content saved in the admin by editors (non-admin users) is stripped of scripts, iframes, forms and event attributes; admins keep full HTML.

Built-in task endpoints

Create a task with one of these URLs and the header {"Authorization":"Bearer <NUXT_CRON_SECRET>"}:

EndpointPurposeSettings needed
/api/cron/send-contact-emailse-mails new contact-form submissions to the ownercontactNotifyEmail (falls back to companyEmail) and a working mail configuration
/api/cron/push-search-enginessubmits URLs to IndexNow (Bing, Yandex, ...) and BaiduindexNowKey, baiduPushToken; blank channels are skipped; /{indexNowKey}.txt is served automatically

Health probe

GET /api/health performs a database round trip and returns driver, storage, version and latency; it answers 503 when the database is unavailable. Point Uptime Kuma, Baota or a Cloudflare health check at it.

Human verification (Admin → Security)

The admin Security page picks the method and the protected forms (admin login, contact form, blog comments, newsletter, shop checkout):

MethodNotes
Built-in image captchano third-party service or keys, works everywhere including mainland China; letters/digits or arithmetic; single-use, valid 5 minutes
Cloudflare Turnstilemost visitors pass without a puzzle; enter the keys or use the environment variables
Google reCAPTCHAv2 checkbox or v3 score (minimum score configurable); script domain www.recaptcha.net works in mainland China
Offrate limits only

New settings must pass one live check before they are saved, so a wrong key cannot lock the admins out. Resolution order:

  1. NUXT_CAPTCHA_PROVIDER (none | local | turnstile | recaptcha): emergency override, e.g. none and redeploy when locked out;
  2. the admin choice;
  3. the site default runtimeConfig.public.captchaDefault (site nuxt.config or NUXT_PUBLIC_CAPTCHA_DEFAULT);
  4. auto: Turnstile when its environment keys are set, then reCAPTCHA keys, else the built-in captcha.

A method without its keys falls back to the built-in captcha, so forms never lock up. Keys can also come from the environment (leave the admin fields blank):

NUXT_TURNSTILE_SECRET_KEY=...
NUXT_PUBLIC_TURNSTILE_SITE_KEY=...
NUXT_RECAPTCHA_SECRET_KEY=...
NUXT_PUBLIC_RECAPTCHA_SITE_KEY=...

Custom site forms: on the page use useCaptcha('contact') (the old useTurnstile() still works and follows the active method), render its container ref, send token and call reset() after each submit; on the server call assertCaptcha(event, token, 'contact') (the old assertTurnstile(event, token) derives the form from the request path). Forms whose custom code cannot render the widget yet are declared in the site's nuxt.config; they are never enforced and shown greyed out in the admin:

runtimeConfig: { public: { captchaUnsupportedScenes: ['contact'] } }

Sites with their own CSP must allow, in script-src and frame-src, https://challenges.cloudflare.com for Turnstile and https://www.google.com https://www.gstatic.com https://www.recaptcha.net for reCAPTCHA. The built-in captcha needs nothing.

Rate limiting

Login, contact, setup and view endpoints are rate limited. Counters live in the rate_limits table, shared by every Worker isolate and Node process; without the table it degrades to per-instance memory.

View counts

GET /api/views?kind=post&id=1 reads, POST /api/views counts one view (max 60 per IP per minute). Data lives in content_views; content tables are untouched. Use useViewCounter('post', () => id); the base blog and product pages already do.

Analytics

NUXT_PUBLIC_CF_ANALYTICS_TOKEN=...   # Cloudflare Web Analytics, cookieless, no consent banner

Admin pages are not tracked.

Startup environment check

At startup the base checks for missing or example secrets and half-configured pairs (Turnstile, object storage) and logs warnings; with NUXT_STRICT_ENV=1 production startup fails instead.

Database migrations

v1.2 adds two SQLite migrations, 0013_rate_limits and 0014_content_views (both IF NOT EXISTS, safe to re-run), plus matching PostgreSQL and MySQL migrations. Deploy with pnpm deploy:migrate, or run the two SQL files against D1 directly.