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).
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).
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).
systhema migrate --schema stops blocking on false positivesLink to this section
Three states real projects were stuck in (#249, #259):
- 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, #258, #260).
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, #255).
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).
⬆️ 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 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).
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).
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).
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).
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, #258, #260).
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).
🐛 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)- 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) - 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) - 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) - Over-long Payload index names are matched by their truncated name instead of blocking on every run (#249)
- Turning Next-only locales off keeps a customised proxy, and
systhema upgradeshows thePreservedlines from itssyncstep (#248) systhema createinstalls with the pinned pnpm, or pins the pnpm that cannot switch (#255)
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) - 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) - 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)
- Two new doctor checks,
package-manager-pin(fast,--fix) andreplaced-proxy(report-only) (#251, #248) CLAUDE.mdpins the robots template to therobots-host-allowlistcodemod; the 1.7.0host-aware-robotscodemod stays frozen (#257)
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)
payloadLink to this section
- feat(payload): build without database access (#253) (0354796d)
🐛 Bug fixesLink to this section
cliLink to this section
- fix(cli): pin new pnpm projects to pnpm 10 (#255) (a52a0ecc)
coreLink to this section
- fix(core): keep a customised src/proxy.ts that has no ledger entry (#248) (b62d0016)
nextLink to this section
- fix(next): return honest statuses for cached 404s and admin file probes (#247) (3f0a1cc6)
next, cliLink to this section
- fix(next,cli): only name allowed hosts in the robots.txt sitemap line (#246) (43ae1bae)
payloadLink to this section
- fix(payload): stop a label added to several enums from blocking itself (#259) (7bb7f6c2)
- fix(payload): write settings hooks into the existing general settings row (#260) (f80fcc79)
- fix(payload): typecheck the database migration scripts (#256) (ccbbb3d9)
- fix(payload): repair bare columns earlier settings hooks added (#258) (11c93a2c)
- fix(payload): unblock default-only drift and long index names (#249) (5328ac93)
- fix(payload): read the postgres identifier limit as a rows array in the rename engine (#250) (4f1649d4)
- fix(payload): create general settings hook columns with payload's definition (#252) (564b043d)
🧪 TestsLink to this section
payloadLink to this section
- test(payload): load lexical builds without the top-level-await shim (#254) (524bd6e3)
📚 DocsLink to this section
claudeLink to this section
- docs(claude): pin the robots template to the robots-host-allowlist codemod (#257) (7bf4b73e)