Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

Troubleshooting

Symptom → cause → fix, with the exact messages to search for.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

Each entry: the symptom as you see it, the cause in one line, the fix. Exact messages are quoted so you can search for them.

Boot and CLI

Missing required environment variable X.

Cause. A required value is unset or empty — DATABASE_URL, or with mail APP_URL / MAIL_FROM / SMTP_HOST, or a storage backend’s bucket/container.

Fix. Set it in <app root>/.env or the process environment. An empty value counts as unset.

Environment variable X must be a positive whole number (got "…").

Cause. 0, -1, 1e9, 0x20 or text in a numeric setting.

Fix. Use a plain positive integer. For “off” values like deliveryIntervalMs: 0, set it in config/*.ts, not .env.

NODE_ENV is "prod", which this app does not recognise …

Fix. Use exactly production, development or test — or leave it unset locally.

CopilotPlugin is enabled but has no model provider.

Cause. COPILOT_ENABLED=true with neither ANTHROPIC_API_KEY nor COPILOT_OPENAI_BASE_URL set.

Fix. Configure a backend, or set COPILOT_ENABLED=false.

MAIL_PROVIDER must be "smtp" or "console" …

Fix. Use smtp (a generated app accepts only smtp), or leave it empty to send nothing.

A setting in .env changes nothing

Cause. Nothing reads it. An unread variable is not an error. Common cases:

  • A generated app does not read SSO_PROVISION_*, SSO_ALLOW_PASSWORD_LOGIN, SSO_SESSION_TTL_SECONDS, COPILOT_MAX_STEPS & co. — see configuration.
  • An app generated before 0.5.2 does not read WEBHOOKS_* — see upgrading.
  • A variable exported in the shell wins over .env.

Fix. Read it in the matching apps/server/config/*.ts with the @orthacms/utils-server readers.

No package.json in … or any parent directory — run this inside an Ortha app.

Fix. cd into the app (any subdirectory works) and re-run.

dist/server/src/main.js does not exist — run `orthacms build` first.

Cause. Not built, or rootDir in a generated tsconfig was changed.

Fix. npx orthacms build. Revert any rootDir change.

The API works, the admin is a 404

Cause. Started outside the app root (staticDir is join(process.cwd(), 'dist/admin')), or built with --server only. The boot log warns that no index.html was found.

Fix. WORKDIR / WorkingDirectory= = app root, and a full npx orthacms build.

Field "post.x" has admin.width "…" — use 'half' or 'full'. / names group "…", which "post" does not declare. / Group "post.seo" has no fields

Cause. A content type’s form layout is invalid; checked at define time.

Fix. admin.width is 'half' or 'full'; every admin.group must be a key of the type’s groups, and every group needs at least one field. See content types.

Migrations

relation "users" does not exist on a fresh migrate

Cause. Plugin order: WorkspacesPlugin before IdentityPlugin. Only a clean database breaks.

Fix. Identity before workspaces in plugins.ts; add new plugins at the end.

Migrating plugin "…" failed — applied N of M plugin(s) before it

Cause. One plugin’s migration failed; earlier plugins are committed, later ones untouched.

Fix. Fix the cause, run npx orthacms migrate again — it resumes.

DATABASE_URL is not set — migrating needs to know which database to change

Cause. The CLI loads .env only from the app root.

Fix. Put DATABASE_URL in <app root>/.env or export it in that shell.

No apps/server/drizzle.config.ts in …

Fix. A generated app ships it; restore it — paths resolve from the app root:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
    dialect: 'postgresql',
    schema: './apps/server/src/content/index.ts',
    out: './apps/server/migrations'
});

joinTableOf throws, or a save hits a missing table

Cause. drizzle-kit diffs top-level exports only; a type’s table or a many-relation’s join table is not re-exported.

Fix. Run npx orthacms content sync — it rewrites the manifest with every table and join table — then npx orthacms generate. See migrations.

Admin

The login rate limit trips for everyone

Cause. TRUST_PROXY unset behind a proxy — every request has the proxy’s IP.

Fix. TRUST_PROXY=1 (per proxy hop), and make sure the proxy sends X-Forwarded-For.

403 Origin not allowed on sign-in or save

Cause. The admin is served from an origin not in ALLOWED_ORIGINS (default http://localhost:4200).

Fix. ALLOWED_ORIGINS=https://cms.example.com (comma-separate several).

Sign-in “succeeds” and does not stick

Cause. Admin and API on different origins — the SameSite=lax session cookie does not follow.

Fix. Serve both from one origin through one proxy. See deployment.

The admin half-works after an upgrade

Cause. A partial upgrade — two copies of a shared package, two React contexts. Nothing errors.

Fix.

npm ls @orthacms/design-system react   # two entries = partial upgrade

Install every @orthacms/* package at one version in one npm install. See upgrading.

Save is missing on a record

Cause. 0.7.0: the record comes from a shared workspace (read-only here), or the type is available only through a shared grant.

Fix. Edit it in its source workspace (Open in source), or grant the type to this workspace as its own.

Public API

401 / 403 / 400 with a token

StatusMessageFix
401Missing `Authorization: Bearer <token>` header.Send Authorization: Bearer orthacms_…
401Invalid API token.Unknown, revoked or expired. Mint a new one — the secret is shown once
403This token’s scope does not allow that operation.A read token on a write or upload — use a full token
403status=draft requires a token with write scope.?status=draft or ?status=any needs a full token
403This token does not cover that workspace.X-Workspace-Id is outside the token’s bucket
400This token covers N workspaces — name the one you want with the X-Workspace-Id header.Add X-Workspace-Id
400Malformed X-Workspace-Id header.It must be a uuid
403This workspace can only use "Tags" records from shared workspaces; it cannot create its own.A write to a type held only via a shared grant. Write in the source workspace

A restricted entry is missing from the API

Cause. The entry’s audiences refuse (or do not admit) the reader. No X-Reader-Tags header = the anonymous reader.

Fix. Send the reader’s tags from your backend:

curl https://cms.example.com/api/v1/content/article \
  -H 'Authorization: Bearer orthacms_…' \
  -H 'X-Reader-Tags: premium, eu'

Renamed the header? Check SEGMENTS_READER_TAGS_HEADER. App generated before 0.7.0? Wire the resolver — see upgrading.

An edited entry still shows the old content

Cause. Expected since 0.7.0. A saved-but-not-republished entry (Modified) keeps serving its published version on REST and GraphQL.

Fix. Publish it.

A localized read returns nothing, or 400 on locale

Cause. No row in that language (there is no fallback on the public API), or the slug is not in config/i18n.ts locales.

Fix. Publish the translation, or read the default locale yourself; use a configured slug.

A field you wrote is not there

Cause. Keys in values that are not declared fields are dropped silently.

Fix. Compare against GET /api/v1/content-types/:name.

Media

curl on /raw prints nothing; a client sees 302

Cause. MEDIA_DIRECT_SERVE=signed-url redirects to the storage backend.

Fix. Follow redirects: curl -L. Do not cache the 302.

Every asset 404s after a redeploy

Cause. MEDIA_LOCAL_ROOT defaulted to ./.storage/media on the container’s disk.

Fix. Mount a persistent volume or use object storage. Old blobs are gone unless the old filesystem survives. See media storage.

A media URL in an img tag is broken in the browser

Cause. The raw route needs the bearer token; browsers do not send it.

Fix. Proxy the bytes through your server, or copy assets to a CDN at build time. See the media API.

Uploads fail with 413 before reaching Ortha

Fix. Raise the proxy’s body limit — nginx client_max_body_size 50m; — to at least MEDIA_MAX_UPLOAD_BYTES.

Webhooks

Use https://. Plain HTTP exposes the payload … or … is a private or reserved address.

Cause. The URL policy: https:// only, no loopback/RFC 1918 targets (checked after DNS, at every send).

Fix (local development only):

WEBHOOKS_ALLOW_INSECURE_URLS=true
WEBHOOKS_ALLOW_PRIVATE_NETWORKS=true

The literal true; 1 reads as off. Still refused? The app predates 0.5.2 and never reads these — see upgrading.