v1.7.5
Migration tooling that meets real databases, gateway and `robots.txt` hardening, builds without a database, and a pnpm pin.
On this page
✨ HighlightsLink to this section
1.7.5 is the batch that came out of consumer projects upgrading to 1.7.4 and reporting back. Most of it is the upgrade and migration tooling meeting real databases: systhema migrate --schema stops blocking on three false positives, the General Settings data hooks create and repair their columns the way Payload's own schema would, and the header rename hook works on PostgreSQL again. Around that are two hardening fixes for the gateway and robots.txt, a build mode for hosts whose database is only reachable at runtime, and a pnpm pin so a project's security overrides and the Lexical admin patch are never silently dropped.
Build without a databaseLink to this section
SYSTHEMA_PRERENDER=off (false and 0 also work) lets next build succeed on hosts where the database is only reachable over a private network, such as Railway's *.railway.internal. generateSysthemaStaticParams returns no params without starting Payload, so pages render on their first request and stay ISR-cached for the route's revalidate. The managed catch-all page.tsx now passes a getPayload getter instead of a promise, because an eager getPayload() started connecting before the flag was read; systhema upgrade refreshes an unedited page.tsx, and a project that patched the file to return [] gets page.tsx.new and can move to the env var. Verified on Railway against a real consumer project: the build makes no database connection, and the runtime reaches PostgreSQL over the private network (#253 (opens in new tab)).
robots.txt names only your own hostsLink to this section
The scaffolded robots.txt route built its Sitemap: line from the client-controlled Host, X-Forwarded-Host and X-Forwarded-Proto headers and sent it with Cache-Control: public, s-maxage=3600, so one forged request could point every crawler at another host's sitemap for an hour. The route now calls resolveSysthemaPublicOrigin from the new @systhemaui/next/origin entry, which accepts a forwarded or Host value only when it names NEXT_PUBLIC_SERVER_URL or one of the project's locale domains, never reads X-Forwarded-Proto, and falls back to the canonical origin otherwise. getSysthemaPublicOrigins() lists the allowed origins. The robots-host-allowlist codemod moves existing projects over (#246 (opens in new tab)).
Honest statuses for unknown pathsLink to this section
A cached 404 was revalidated into 304 Not Modified: Next hashes every non-dynamic payload into an ETag without looking at the status, and the catch-all's notFound() render is an ISR entry with status 404. The gateway now drops If-None-Match from page requests, so the origin answers 404 again; responses keep their ETag, so Cloudflare keeps caching pages and answering conditional requests at the edge, and public/ files keep their 304s. An anonymous request for an Admin file probe such as /admin/.env got Payload's 200 page because AuthProvider renders nothing for a logged-out SSR; a dotted Admin path without a Payload session cookie is now rewritten to the site's 404. Every Payload project gets both through its generated src/proxy.ts (#247 (opens in new tab)).
systhema migrate --schema stops blocking on false positivesLink to this section
Three states real projects were stuck in (#249 (opens in new tab), #259 (opens in new tab)):
- A column that differs only in its default is planned as
ALTER TABLE … ALTER COLUMN … SET DEFAULTorDROP DEFAULT. It rewrites no row, so a plugin release that moves a default (@payloadcms/plugin-import-export3.90 did, onexportsandimports) no longer needs a hand-written migration. Defaults PostgreSQL writes back in another form (jsonbkey order, casts, parentheses) are compared by value, so they are not re-planned on every run. - Index names past PostgreSQL's identifier limit (63 bytes by default), which Payload generates for deeply nested arrays, are matched by the truncated name PostgreSQL actually stored instead of blocking on every run. Names that collide after truncation still block.
- A label added to several enums at once, as a new locale is to
enum__localesand each per-locale publishing enum, no longer trips the same-transaction safety check on its ownADD VALUE, and a new enum that merely declares the label does not block either. The guard also catches enum-array defaults and explicit casts to the extended enum, which used to slip through and fail mid-run with55P04.
General Settings hooks that match Payload's schemaLink to this section
The SEO and Emails General Settings data hooks (migrate-seo-data, migrate-emails-data) added bare nullable varchar and integer columns, and because systhema upgrade runs them before any schema addition, migrate --schema then blocked on seo_title_separator and seo_title_template, and a SQLite push rebuilt general_settings. The hooks now add each column with the definition Payload's schema gives the field: its default, NOT NULL when required, the PostgreSQL enum of a radio or select, and the SQLite upload foreign key. On PostgreSQL they also repair the bare columns an earlier release's hook left behind, bringing each one to its default, NOT NULL and, when that is safe, its type. And they write into the settings row that exists instead of assuming id = 1: PostgreSQL projects with UUID IDs failed with column "id" is of type uuid but expression is of type integer, and SQLite UUID projects got a second row keyed "1" that Payload never read. A missing row is created with the copied values and the ID Payload would give it (#252 (opens in new tab), #258 (opens in new tab), #260 (opens in new tab)).
Pin pnpm, or lose your overridesLink to this section
pnpm 11 and later print one warning and then ignore the pnpm field of package.json, where every Systhema scaffold keeps its security overrides, onlyBuiltDependencies and the Lexical admin patch. A project installed with pnpm 11 or 12 got vulnerable transitive versions and no patch. systhema create now pins new pnpm projects to pnpm@10.34.6 through packageManager, which pnpm 11+ honour by switching, and the new fast package-manager-pin doctor check warns on an existing project with no exact pin or with a pnpm that cannot install its pnpm-lock.yaml (the ERR_PNPM_LOCKFILE_CONFIG_MISMATCH a build host prints). systhema doctor --fix writes a pnpm version that fits. New projects also get pnpm.overrides floors for nanoid 3.3.18 and js-yaml 4.3.2, and the fast-uri floor rises to 3.1.8 (#251 (opens in new tab), #255 (opens in new tab)).
A customised src/proxy.ts survives the upgradeLink to this section
create-app-files overwrote a customised src/proxy.ts in a project without a managed-files ledger entry for it, such as one upgrading from before 1.7.1, so its rate limiting, CSP and security headers survived only in a one-time .bak. A proxy matching one Systhema has shipped is still refreshed; any other keeps its bytes and the current template is written to src/proxy.ts.new. Disabling Next-only locales keeps a customised proxy the same way, systhema upgrade replays the Preserved lines from its sync step, and the new report-only replaced-proxy doctor check warns when src/proxy.ts is a stock proxy while src/proxy.ts.bak matches no proxy Systhema shipped, which is the trace an earlier upgrade leaves (#248 (opens in new tab)).
⬆️ UpgradeLink to this section
pnpm add -g @systhemaui/cli@latest
cd <your-project>
systhema upgrade --dry-run
systhema upgrade --yes --allow-databaseOne codemod runs: robots-host-allowlist rewrites src/app/robots.txt/route.ts when it matches a version Systhema shipped, ignoring whitespace. A customised route that still reads the host headers is skipped and reported; switch it to resolveSysthemaPublicOrigin from @systhemaui/next/origin yourself.
Re-run the General Settings hooks on PostgreSQL if a 1.7.1 to 1.7.4 upgrade left bare seo_* or emails_* columns that block systhema migrate --schema. systhema upgrade --allow-database runs them again; so does systhema migrate migrate-seo-data and systhema migrate migrate-emails-data. A re-run repairs the bare columns and does nothing when none are left.
Re-run systhema migrate migrate-header-nav-dbnames on PostgreSQL if it crashed with Cannot read properties of undefined (reading '0'). It renamed nothing in that state, so the re-run does the whole job.
Pin pnpm. Run systhema doctor and let --fix write packageManager, or add "packageManager": "pnpm@10.34.6" to package.json yourself. Build hosts that follow packageManager (Railway, Coolify, Vercel, Corepack) switch to it. The new security floors reach new projects only; copy the nanoid, js-yaml and fast-uri entries from scripts/security-overrides.json (opens in new tab) into your pnpm.overrides and run pnpm install.
If your build host cannot reach the database, set SYSTHEMA_PRERENDER=off in the build environment. The first visitor to each page after a deploy waits for a server render; publishing still revalidates pages as before.
If you had patched src/app/(site)/[[...segments]]/page.tsx to skip the build-time query, the upgrade writes page.tsx.new; take the managed file and set the env var instead.
⚠️ Breaking & Behavioral ChangesLink to this section
1. robots.txt no longer reflects a forwarded hostLink to this section
The route's Sitemap: line names the origin of NEXT_PUBLIC_SERVER_URL or one of the project's locale domains, whichever the X-Forwarded-Host or Host header matches; any other value, and any X-Forwarded-Proto, is ignored. A tunnel that sets X-Forwarded-Host to an internal name falls through to an allowed Host. systhema upgrade rewrites a shipped route; a customised one is reported (#246 (opens in new tab)).
2. The gateway answers pages without 304s and 404s Admin file probesLink to this section
Page requests lose their If-None-Match before reaching Next, so the origin never answers a cached 404 with 304. The cost is that an expired edge copy fetches the full page instead of a 304; /api, /_next, /sys, /trpc and /_vercel keep their conditional requests, and public/ files keep If-Modified-Since. Anonymous requests for an Admin path with a dot in a segment (/admin/.env, /admin/wp-login.php) get the site's 404; logged-in requests still reach Payload, so a document ID containing a dot keeps working (#247 (opens in new tab)).
3. create-app-files keeps an unknown src/proxy.tsLink to this section
A pre-ledger src/proxy.ts that matches no proxy Systhema shipped is no longer replaced; the template goes to src/proxy.ts.new and the run logs Preserved src/proxy.ts. A stock proxy with a digest missing from the list gets .new too instead of a refresh. Files under (systhema) keep the .bak refresh. Disabling Next-only locales deletes src/proxy.ts only when its digest is known; any other existing proxy still stops the locale migration (#248 (opens in new tab)).
4. New pnpm projects declare packageManagerLink to this section
systhema create writes "packageManager": "pnpm@10.34.6". --pm npm and --pm yarn projects do not get it, because Corepack refuses to run another package manager in a project that names pnpm. The bundled templates take the pin from the monorepo's own package.json, and the CLI build fails if that pin ever moves to pnpm 11 or later (#255 (opens in new tab)).
5. The General Settings hooks write typed columns and the existing rowLink to this section
Columns the SEO and Emails hooks add carry Payload's default, NOT NULL when the field is required, the enum of a radio or select, and the SQLite foreign key of an upload. An existing general_settings row takes those defaults, the same as a push would set them, and a value equal to the column default no longer counts as data that stops the legacy copy. On PostgreSQL a re-run repairs bare columns from an earlier hook: type and default change in one statement, so a stored value the enum rejects changes nothing and is logged; a non-enum type change happens only while the column holds no values. The hooks write into the row that exists (ORDER BY id LIMIT 1) and create one with the copied values when there is none, using a serial, 1 for a custom number ID, a UUID for a custom text ID or idType: 'uuid', and a version 7 UUID for uuidv7. A row that can neither be read nor created fails the hook instead of writing locale rows with _parent_id = NULL (#252 (opens in new tab), #258 (opens in new tab), #260 (opens in new tab)).
6. systhema migrate --schema plans default changesLink to this section
A default-only difference used to be a blocker that needed a project-owned migration. It is now planned as SET DEFAULT or DROP DEFAULT, appended after the generated additive DDL and before enum additions, so the same-transaction enum-label check still covers a default that names a newly added label. Type, nullability, identity, generated and serial differences block as before (#249 (opens in new tab)).
🐛 Fixes & Internal ImprovementsLink to this section
Bug FixesLink to this section
migrate-header-nav-dbnamesworks on PostgreSQL — the rename engine read the identifier limit with the{ rows }shape while its client contract returns the rows array, so every PostgreSQL project crashed before the first rename. A contract-shaped test now drives the PostgreSQL path (#250 (opens in new tab))- Settings hooks on UUID projects — PostgreSQL projects with UUID IDs failed the hook, and SQLite UUID projects got a second settings row keyed
"1"(#260 (opens in new tab)) - Bare hook columns are repaired — a database where an earlier hook added nullable, default-less columns no longer blocks
migrate --schemaafter a re-run (#258 (opens in new tab)) - A new locale no longer blocks
migrate --schemaon a project with localized drafts, and enum-array defaults and casts to a freshly extended enum block instead of failing mid-run (#259 (opens in new tab)) - Over-long Payload index names are matched by their truncated name instead of blocking on every run (#249 (opens in new tab))
- Turning Next-only locales off keeps a customised proxy, and
systhema upgradeshows thePreservedlines from itssyncstep (#248 (opens in new tab)) systhema createinstalls with the pinned pnpm, or pins the pnpm that cannot switch (#255 (opens in new tab))
Internal / MonorepoLink to this section
- The payload typecheck covers
src/scripts/— the drizzle snapshot inschemaMigration.tswas silently typedany, which is how the rename-engine mismatch shipped; 53 type-only fixes (#256 (opens in new tab)) - No more exit 13 in the payload tests on Node 22 — a resolve hook sends each Lexical
*.node.mjsshim straight to the build it would have loaded, so the module graph has no unsettled top-level await (19 of 150 runs failed before, 0 after) (#254 (opens in new tab)) - The General Settings hook tests stay off the schema-migration advisory lock, which is keyed per database while CI shares one database across parallel test files (#252 (opens in new tab))
- Two new doctor checks,
package-manager-pin(fast,--fix) andreplaced-proxy(report-only) (#251 (opens in new tab), #248 (opens in new tab)) CLAUDE.mdpins the robots template to therobots-host-allowlistcodemod; the 1.7.0host-aware-robotscodemod stays frozen (#257 (opens in new tab))
List of all changesLink to this section
🚀 FeaturesLink to this section
cliLink to this section
- feat(cli): add a pnpm pin doctor check and new advisory floors (#251) (35e709a0 (opens in new tab))
payloadLink to this section
- feat(payload): build without database access (#253) (0354796d (opens in new tab))
🐛 Bug fixesLink to this section
cliLink to this section
- fix(cli): pin new pnpm projects to pnpm 10 (#255) (a52a0ecc (opens in new tab))
coreLink to this section
- fix(core): keep a customised src/proxy.ts that has no ledger entry (#248) (b62d0016 (opens in new tab))
nextLink to this section
- fix(next): return honest statuses for cached 404s and admin file probes (#247) (3f0a1cc6 (opens in new tab))
next, cliLink to this section
- fix(next,cli): only name allowed hosts in the robots.txt sitemap line (#246) (43ae1bae (opens in new tab))
payloadLink to this section
- fix(payload): stop a label added to several enums from blocking itself (#259) (7bb7f6c2 (opens in new tab))
- fix(payload): write settings hooks into the existing general settings row (#260) (f80fcc79 (opens in new tab))
- fix(payload): typecheck the database migration scripts (#256) (ccbbb3d9 (opens in new tab))
- fix(payload): repair bare columns earlier settings hooks added (#258) (11c93a2c (opens in new tab))
- fix(payload): unblock default-only drift and long index names (#249) (5328ac93 (opens in new tab))
- fix(payload): read the postgres identifier limit as a rows array in the rename engine (#250) (4f1649d4 (opens in new tab))
- fix(payload): create general settings hook columns with payload's definition (#252) (564b043d (opens in new tab))
🧪 TestsLink to this section
payloadLink to this section
- test(payload): load lexical builds without the top-level-await shim (#254) (524bd6e3 (opens in new tab))
📚 DocsLink to this section
claudeLink to this section
- docs(claude): pin the robots template to the robots-host-allowlist codemod (#257) (7bf4b73e (opens in new tab))