* docs: add agents-only pointer to agents.insforge.dev on introduction page
Adds a Mintlify <Visibility for="agents"> block below the existing hidden
agent instruction, pointing agents to the Sherpa-run agent-workflow site.
Renders only in the .md/agent view, invisible on the human web page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZaoLMs3PseiLeivXfZWwH
* docs: mirror agents-only pointer in es/zh/zh-Hant introductions
Per DOCS_I18N, locale trees mirror English structure; agent-facing text
stays English, matching the existing hidden agent div in these files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZaoLMs3PseiLeivXfZWwH
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
**The startup race both bots flagged is real, and its effect is permanent.** The
listener opens before stored credentials and settings are read, and the provider
registry is a field initialiser — so a compute request landing inside those two
reads constructs the registry from environment-only values and that choice sticks
for the life of the process. A provider configured only through the dashboard would
stay missing until the next config write. The registry is now discarded after
priming, so an early build cannot outlive it.
**A create from a non-default provider's page went to the wrong driver.** The
dialog never received a provider: it read the default provider's capabilities and
the request carried no selection, so on `/dashboard/compute/fly` with Docker as
default the service was created on Docker and then filtered out of the page that
created it. `createServiceSchema` takes an optional `provider`, the service layer
resolves the driver from it, and a named provider that is not configured is
rejected rather than silently swapped — a row that disagrees with where the
container runs is the failure the capability layer exists to prevent. Omitting the
field still uses the default, so the CLI is unaffected.
Also from the review:
- `updateConfig` re-primes in a `finally`. A failure part-way through left earlier
writes persisted while the in-memory snapshot still described the pre-write state.
- The landing redirect validates `configured[0]` against the slugs the dashboard has
pages for. An unrecognised one redirected here, got bounced back by the page's own
slug guard, and looped.
- `DockerSettingsForm` seeds once. A refetch while someone was typing replaced their
input with what the server still had.
- Both settings forms catch `mutateAsync` rejections — `void handleSave()` left them
unhandled. The Fly form also keeps its fields on failure: it holds a pasted token,
and clearing it made the retry a re-paste.
- The save toast said "Fly credentials saved" after saving Docker settings; both
forms share the mutation. Now provider-neutral, in four locales.
- `ComputeLayout.test.tsx`: `beforeEach` did not reset `meta.value`, so the spinner
test depended on execution order, and `enabledCalls.every(...)` passed on an empty
array, so the fail-fast gate was unverified if the hook never ran.
- The dev compose comment described the production image's entrypoint. That service
builds `target: dev`, which runs as root and needs no group handling.
- Spanish docs: singular verb after the line count changed to one.
Declined, with reasons in the PR thread: the ast-grep SQL-injection flag (column
names come from a fixed map, values are parameterised), the prettier violation
(prettier and eslint are both clean on that file), and a cross-field Zod refine on
`computeCapabilitiesSchema` (nothing parses it at runtime, so it would be dead code
that could only hard-fail metadata if it ever ran).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enabling Docker took three steps because the production image runs as `node` and
the socket is mode 660 root:docker, so a matching group was required. The group id
is host-specific — 0 on Docker Desktop, commonly 999 on Debian/Ubuntu, 993 on
Amazon Linux 2023 — and a wrong value is a silent EACCES that shows up as "Docker
never appears in the dashboard". There is no value that is right everywhere, so
there is no default to ship.
The container can read it off the socket. The runner stage no longer sets
`USER node`; a new entrypoint starts as root, takes the socket's group with
`stat -c %g`, joins it, and execs the command through `su-exec node`. The app
process is still `node` and `tini` is still PID 1. With no socket mounted it
drops to `node` and does nothing else, and a deployment that pins `user:` is
left alone.
`group_add` and `DOCKER_GID` are gone from all five compose files, `.env.example`,
the docs in four languages, the dashboard guide (three steps to two) and the
not-configured API message. Uncommenting one volume line is now the whole edit.
The mount stays opt-in: the socket is root-equivalent on the host, and on by
default would hand every deployment's backend the ability to take over its host
whether or not it uses compute.
Verified on a build of `target: runner` — no socket: uid 1000; socket at gid 0:
joins root, writable; socket at gid 999 with the group already present: joins it,
writable; and the old `USER node` path with no entrypoint: denied, which is the
failure the removed step existed to avoid. Then end to end from that image
against a live database: provider ready, metadata slice present, a service
created and serving on its published port.
Also batches the compute logs limiter test. 120 sequential requests overran the
10s timeout on a busy machine and 120 simultaneous ones had connections dropped;
ten in flight is neither, and the assertion is by count rather than position.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both commands are real — `fly tokens create org` is "Create org deploy tokens"
and `fly orgs list` is "Lists organizations for current user", checked against
flyctl v0.4.60 — but the copy around them was wrong. Rendering the page in a
browser rather than reading the source showed why: the locale files still held an
older design's strings, and a locale value beats a component's defaultValue.
- Step titles carried a leading "1."/"2."/"3." while StepItem renders the
circled number, so every step showed its number twice.
- Fly's step 2 read "Set both values in .env" — the screen it labels has the
form in it, and .env is exactly what it exists to avoid.
- The commands ran in the wrong order with no `-o`. `tokens create org` takes the
slug, and without it flyctl stops to ask for an org you have not looked up yet.
.env.example already had the flag; the panel had dropped it.
- The field hints repeated the command with literal backticks. They now say what
the value is — an org slug is not a display name, which is a real mistake to
make.
Also strips a leading `FlyV1 ` when a token is stored. The CLI prints the
macaroon with its scheme attached, and copying that line as printed is the
obvious thing to do — but the logs endpoint prepends `FlyV1 ` itself (it rejects
`Bearer`, verified live in the container-logs work), so a verbatim paste would
send the scheme twice and break the logs panel while machines kept working. A
token without the prefix is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes the failing vale-spellcheck and the valid review comments.
Vale rejected Traefik and SELinux, both legitimate proper nouns; added to the
Mintlify accept list, which already carries Coolify, Dokploy, flyctl and nginx.
Docs, all four locales:
- The ingress default was stated as universal (`none`), contradicting the capability
table further down the same page which shows Fly offering only a hostname. It now
says the modes and the default both depend on the provider, and that a mode the
provider cannot give is coerced rather than rejected.
- The build example used `$ID` without ever assigning it, so copy-pasting it posted
to `/api/compute/services//build`. Step 1 now captures the id from the response.
Dashboard:
- ServiceCard looked capabilities up by the deployment's *default* provider while
each service row carries its own. Providers coexist and the backend routes per
row, so a Docker service in a Fly-default deployment showed the meaningless region
`local`, and a Fly service in a Docker-default deployment hid the region it
genuinely has. The hook now takes an optional provider name; the card passes the
service's own, the create form keeps using the default.
- The ingress chip rendered the raw enum (`port`) because the i18n keys did not
exist. `compute.fields.ingress` and `compute.ingressModes.*` are now defined in
all four locale files, alongside the region keys that were already there, and the
chip falls back to the same friendly label table the create dialog uses.
- Corrected the comment on the capability fallback. Region falls back to shown and
ingress to hidden, which looks inconsistent but is deliberate in both directions:
a pre-capability backend is a Fly backend where regions are real, and it has no
ingress field at all, so an ingress control there would silently do nothing.
Tests: the ingress case now opens the select, asserts the options are filtered to
what the provider reported, picks one and checks it reaches the payload — it
previously only checked the field's presence and the default. Four new ServiceCard
tests cover the per-provider lookup, the friendly label, region retention for a
region-capable provider, and the unknown-provider fallback. Radix's Select needs
pointer-capture and scroll shims under jsdom; no other test in this package opens
one. Both fixes verified by reverting them and watching the tests fail.
Not changed: CodeRabbit asked for the hook filename in kebab-case per a repo
guideline, but all 64 hooks in this package are camelCase and none are kebab-case,
so the new file matches its siblings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the self-hosted Docker driver (#1892), which shipped without any of the
user-facing surface.
Docs, all four locales. The compute overview still said containers run on Fly.io and
told self-hosters to bring a Fly account. It now documents both providers behind
tabs, the socket-mount opt-in with the host-specific DOCKER_GID (and the Docker
Desktop difference, where the socket is root:root so the value is 0), the three
ingress modes with the loopback bind default, source builds through
POST /:id/build, the platform support tiers, and a capability table so a reader can
see what Docker cannot do. Two claims on that page were false and are now true:
nothing is injected into a container's environment — verified across the OSS
service layer, the Fly provider and the cloud backend, all of which send only the
caller's env vars — and there is no replica count, so the page no longer offers to
scale horizontally.
Dashboard. The create dialog offered a region picker to every provider, so a
self-hoster on Docker picked a region that went nowhere. It now reads the compute
slice of /api/metadata: region is hidden and omitted from the payload when the
provider has none, and an ingress control appears when more than one mode exists —
without which every dashboard-created service on Docker would be private and
unreachable, since `none` is the default. Undefined capabilities means "not known
yet" (loading, compute off, or a backend older than the slice) and shows everything,
which is the previous behaviour and stays correct against an older backend. The card
shows ingress instead of the region chip where region is meaningless.
Adds CreateServiceRequestInput (z.input) so a caller can legitimately omit a field
that has a schema default, following the ...BodyInput precedent in the payments
schema. Four dashboard tests cover the gating in both directions; verified by
reverting the gate and watching the Docker cases fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ship a maintained backup script that pg_dump's Postgres and copies .env,
include it in the setup.sh sparse checkout manifest, and point the
deployment security guide at the script instead of an inline copy-paste.
Add a step-by-step deployment guide for Hetzner Cloud that follows
the existing AWS EC2 walkthrough pattern: server and firewall setup,
deploy/setup.sh bootstrap, verification, and optional TLS via Nginx.
Update the deployment README and docs nav. Firewall guidance matches
the self-host compose file (internal services on 127.0.0.1 only).
Closes#1909
The rollback section and setup.sh's header had grown more explanation than
instruction. Section 16.2 is four numbered steps now, with one sentence on the
consequence of skipping the last one; the three paragraphs justifying the
approach are gone. setup.sh's header keeps the usage lines and the environment
variables and drops the reasoning behind them, and the same trim runs through the
comments inside — 259 lines to 243.
INSFORGE_REF now says what it accepts: a tag, branch or commit. It takes all
three (raw.githubusercontent.com resolves each, and git clone --branch covers the
first two), and the previous wording implied releases only.
Also removes a stray blank line and shortens the two access-key comments in
.env.example, left over from the variables that came out of this branch.
Re-ran the setup.sh matrix after the trim, since comment edits in a shell script
are not as safe as they look: both acquisition modes, ref pinning in each, all
three guards, and the fail-closed path — 10 checks, no failures. The structural
checks pass too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps cubic found in the rollback procedure.
It said to append pin.yml to COMPOSE_FILE and never said to take it out. Section
15's update then pulls new images and keeps running the pinned one — the pin wins
over latest, silently, for as long as the entry is there. So a rollback that was
meant to be temporary becomes permanent, and looks like updates that stopped
working.
And the example showed a whole COMPOSE_FILE line, while the instruction said to
append. Anyone with a storage overlay already in that variable would copy the
example and drop MinIO or RustFS out of their stack. The example keeps the old
value in a comment above it now, so what is being appended to is visible.
Four locales.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
INSFORGE_OSS_VERSION and INSFORGE_DEPLOYMENT_METHOD do not earn their place. A
caller can already override an image and add environment variables the compose
file never mentions, by appending an overlay to COMPOSE_FILE — the mechanism the
storage overlays use, documented in .env.example, needing nothing from here:
services:
insforge:
image: ghcr.io/insforge/insforge-oss:v2.2.9
That is strictly better than the variable it replaces: it pins any image in the
stack, not only this one.
I also overstated the case for the version variable. I said a rollback that
edited the compose file would leave the install unable to update, because
`git merge --ff-only` refuses when a tracked file has local changes. It refuses
only when upstream also changed that line; otherwise the merge succeeds and
keeps the edit. The overlay avoids the question entirely by being a file of the
operator's own, and section 16 now teaches that instead.
Dropping the telemetry stamp also avoids a behaviour change nobody asked for:
with a `docker-compose` default, every existing self-host would have started
reporting `docker-compose` where it reports `docker` today.
ACCESS_API_KEY and ACCESS_ANON_KEY stay, for a reason that has nothing to do
with any caller: .env.example documents both as settable, and this compose file
silently dropped them, so a self-hoster who set them saw no effect. That is the
same inconsistency as section 5.4 telling people to set AWS_* variables the file
does not pass.
Verified on a running stack: the overlay pins insforge-oss to v2.2.9, adds
INSFORGE_DEPLOYMENT_METHOD which the base file never names, and the injected
ACCESS_API_KEY answers 200 on /api/database/tables.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 16 told the reader to edit docker-compose.yml to pin an older image.
That fights section 15: updating runs `git merge --ff-only`, which refuses when
a tracked file has local changes. So following the rollback once left the
install unable to update until the edit was reverted — and nothing said so.
It sets INSFORGE_OSS_VERSION in .env now, which is the file that belongs to the
operator and that no update touches. That is what the variable is for, and this
is the procedure that needed it.
Two things in that section were also no longer true:
- It said `deploy/docker-compose` pins v1.5.0 and the project is on 2.x. That
file pins insforge-oss to latest, and the other three images to their own
upstream versions.
- It opened by restoring `docker-compose.yml.old`, a file no step in this guide
ever creates. Section 14 backs up .env and records image versions, which is
what the rewritten procedure uses.
16.2 and 16.3 collapse into one step, so the rest of the section renumbers.
Four locales.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The steady trickle of small findings has one cause: I have been fixing this
corpus with grep-driven sweeps, and every pattern has been narrower than the
corpus. 30 of these files are near-duplicate translated prose, so each logical
fix landed in twelve files and missed four — the variants being case
(`~/InsForge` vs `~/insforge`), indentation (fences nested in list items),
translated phrasings that do not match the English anchor, and code comments
rather than prose. Reviewers then found the remainder, one batch at a time.
So this pass wrote the invariants down and checked all 34 changed deploy files
against them mechanically. Thirteen rules, ten violations, all fixed:
- Azure's update commands used `~/InsForge` while its setup step creates
`~/insforge` — case-sensitive on Linux, so the documented update fails before
it starts. Four locales.
- The image-only compose probed `pg_isready -U postgres` with POSTGRES_USER
configurable one line above, so overriding the user left Postgres permanently
unhealthy and, since insforge gates on it, never started the backend. The same
class of bug as the postgrest healthcheck, in the same file. Verified: with
POSTGRES_USER=ifadmin the container now reaches healthy.
- The update procedure's review diff covered `deploy` and `.env.example` but not
`functions/`, which is in the sparse checkout and which the Deno service
executes. Four locales.
- Section 5.2 still told the reader to set POSTGRES_PASSWORD, two sections after
4.2 started the stack — and Postgres only reads it at cluster init, so the
instruction could not work. It says so now. Four locales.
- setup.sh only added COMPOSE_FILE when absent, so a .env copied from the
template by hand kept pointing at the development stack, whose files the
checkout does not contain. It now repoints that exact value, and leaves
anything else — including a hand-appended storage overlay — alone.
- MD040 on two fences, and the update prose that called an update "a pull and
restart" when the checkout carries Postgres's config and the Deno functions.
The audit also caught something no reviewer did: .env.example's COMPOSE_FILE has
to stay equal to what Compose discovers on its own in that directory, which
changed when docker-compose.override.yml was deleted earlier in this branch.
It is equal — verified byte-for-byte against auto-discovery — and setup.sh's
self-heal constant now has to match it, with both sides commented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From cubic's pass, after reproducing each:
- setup.sh's migration branch exits before the fresh-install path's `chmod 600`,
so an install whose old .env was group-readable stayed that way. It now
chmods right after the move; verified by handing it a 644 file.
- The multi-instance section had `cp .env.example .env.projectN` with no further
instruction, and .env.example now ships COMPOSE_FILE's development value —
a file the sparse checkout does not contain. Each copy needs the production
path.
- ROOT_ADMIN_PASSWORD is mandatory in the platform files now, alongside
JWT_SECRET and ENCRYPTION_KEY. Unlike POSTGRES_PASSWORD there is no migration
hazard: auth.service.ts hashes it from the environment on every boot rather
than persisting a record, so changing it takes effect on restart. A dashboard
reachable from the internet on `changeme123` is the most directly exploitable
of the three placeholders.
- The zh, zh-Hant and es guides' section 5.5 still listed AWS_S3_BUCKET/AWS_*,
which the compose file does not pass, while English had moved to S3_*. The
note I added to 5.4 pointed at "the S3_* variables in 5.5" — which did not
exist in those three. Aligned on the names the compose actually reads.
- GCE's backup snippet still said "run from deploy/docker-compose/" in a code
comment, which the earlier path sweep did not match.
- Containarium's agent flow wrote the whole of ~/insforge/.env, overwriting the
secrets setup.sh had just generated two steps earlier.
Also updates the backend skill note, which claimed the two platform files differ
only in INSFORGE_DEPLOYMENT_METHOD; the build context differs too, since Coolify
builds with --project-directory and Dokploy does not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review on #1887 caught two real gaps.
The platform files dropped VERCEL_TOKEN, VERCEL_TEAM_ID and VERCEL_PROJECT_ID
— the only three variables the image-only compose passes that they did not —
so site deployments and custom domains ran without credentials. Both guides
tell the reader that .env.example lists every supported variable, which made
that silent.
None of the cloud guides re-ran setup.sh after `git pull` in their update
sequence. A release that adds a file the compose reads also adds its path to
the script, and without re-applying, the merge lands the file in git but never
in a sparse working tree — the stack then starts against a file that is not
there. Only the security guide had this step.
While checking those: sixteen prose references still pointed readers at
`~/insforge/deploy/docker-compose` as the directory to run commands from. Four
were the Azure guide's `~/InsForge/...`, which the earlier case-sensitive sweep
missed.
Also settles the Coolify verification gap this PR shipped with. Coolify binds
domains per compose service through `docker_compose_domains` on the
application, which was null, which is why its auto-assigned FQDN returned 404.
Set to bind insforge, a real deploy answers /api/health over the domain, and an
edge function created and invoked through it returns the worker's body — as
does hitting http://deno:7133 directly, so service-name resolution works under
Coolify's networking. Verified with real secrets: PATCH, not POST, is what
updates an environment variable Coolify already discovered from the compose
file.
Vale flagged three words in the new guides. Coolify joins Dokploy and Zeabur in
the vocabulary; "undecryptable" and "preloads" are rewritten rather than added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The quickstart is the setup.sh one-liner, with a download-and-read variant
for anyone who will not pipe a script to a shell. The old "do the same by
hand" block reproduced the script's sparse-checkout list, which had already
drifted once when the list gained deploy/setup.sh and the copy did not.
- Every command runs from the checkout root. The four cloud guides (AWS EC2,
Azure VM, GCE, Containarium) cloned the full monorepo — 1,782 files, 57.6MB —
and then ran the image-only compose file anyway; none reference a build step.
They use setup.sh now, which also stops walking the reader through inventing
JWT_SECRET by hand or pasting a placeholder over a generated secret.
- New Coolify and Dokploy guides. Both platforms had compose files in the repo
and no documentation; Coolify appeared once, in a list of telemetry values.
- COMPOSE_PROJECT_NAME is documented with its consequence: any .env on the host
carrying the same name means `docker compose up` there adopts and recreates
that project's containers.
- self-host-storage.mdx covers the image-only stack, which it never did —
appending an overlay to COMPOSE_FILE rather than passing -f flags.
- Section 5.4 no longer tells self-hosters to set AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY; the compose file passes neither.
- The automated backup script pointed -f at a compose file the checkout does
not contain, and -f alone loads neither COMPOSE_FILE nor
COMPOSE_PROJECT_NAME from .env, so its `exec` would not have found the
container.
Registered in docs/deployment/README.md and docs.json's English tree, locale
trees regenerated with scripts/build-docs-langs.py. The i18n parity check
reports the same 24 pre-existing gaps as before.
Not verified: the domain-assignment step in the two platform guides. Coolify's
auto-assigned application FQDN returns 404 because a compose deployment binds
domains per service, and I found no API field for that; the Dokploy stack was
checked over the container network. Both steps follow the platforms' own docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the Mintlify PostHog integration to docs.json using the same
public project key (phc_) the dashboard already reports to. Events
route through Mintlify's default proxy (ph.mintlify.com); session
recordings additionally require the docs domain to be added to
PostHog's authorized domains for recordings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NcFYkLqoAuXW2fPbNm11V9
openapi/deployments.yaml and openapi/dashboard.yaml are maintained in the repo
but never referenced from docs/docs.json, so neither reaches the docs site. The
other fourteen specs each appear once per language. That leaves 15 documented
endpoints, 14 deployments paths and 1 dashboard path, with no published
reference: https://docs.insforge.dev/api-reference/deployments returns 404.
Register both specs in all four language navigations, matching the existing
entry shape. Group names are left in English because these navigations already
keep Tables, Records and Schedules in English; happy to localise them if you
would rather.
Excluding 403 alongside 401 was wrong. A 403 usually means the request
did reach the feature and was denied by policy: RLS denials surface as
403 (SQLSTATE 42501 maps to it in errors.ts), storage permission checks
raise it in six places, and disabled signups return it from auth. Those
requests exercised the feature, so dropping them undercounted exactly
the projects that configure RLS properly.
Only 401 is excluded now, which still covers both cases the exclusion
was added for: an unauthenticated probe, and the rejected read a
dashboard tab fires when its 15-minute token expires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Telemetry previously reported only whether optional services were
configured, which says an admin pasted an API key, not that anyone uses
the feature. Heartbeats now carry the set of features touched since the
previous heartbeat.
It is a yes or no per feature, not a count: no request volumes, paths,
payloads, or anything about who made the request. Reads from a dashboard
session are ignored, since a tab left open would otherwise mark half the
product as used, and dashboard sign-in traffic under /api/auth/admin is
excluded for the same reason. Dashboard writes still count.
The S3 protocol gateway and direct edge-function invocations are counted
toward storage and functions: both bypass the /api router, and both are
the surfaces a finished app actually calls.
Shutdown now sends a final heartbeat, so instances that run for less
than the heartbeat interval still report.
Unchanged on InsForge Cloud, which is gated twice: the middleware is
never registered, and the service never starts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Self-hosted deployments previously supported manual backups only. This
adds an in-process backup scheduler with retention, configured through a
new Database Settings dialog in the dashboard.
Backend:
- system.database_config singleton (migration 061) holding backup_enabled,
backup_cron_schedule, and backup_retention_days, following the
realtime/schedules config conventions; future database-level settings
share this table.
- Node-side scheduler in DatabaseBackupService: a one-minute tick computes
due-ness from persisted state (most recent UTC cron fire vs last
scheduled attempt and last config change), so missed fires run shortly
after a restart and enabling a schedule never fires retroactively.
pg_cron is not used because backups need the backend to spawn pg_dump
and stream artifacts through the storage provider.
- Cron expressions are free-form 5-field, validated to fire at most once
per hour (single literal minute field).
- Retention prunes expired scheduled backups (artifact included) right
after each successful scheduled run, while the in-flight guard still
blocks restores. Manual backups are never auto-deleted.
- GET/PATCH /api/database/backups/config (admin, audited); the whole
backups router remains unmounted in cloud environments, and the
scheduler only starts outside them.
Dashboard:
- Database Settings dialog (gear on the Database sidebar, Configure on
Backup & Restore): enable toggle, preset chips + cron input with
client-side validation and a next-backup readout, retention select.
Schedule controls gray out while disabled. Cloud mode never shows it.
- Backups page renders the Scheduled Backups section in self-hosting with
status, restore, and delete per row.
- New i18n keys in en/es/zh-CN/zh-TW.
Tests:
- Unit: schedule helpers (due-ness, validation), scheduler/prune/config
service paths, migration guards, settings dialog component tests.
- Integration (real Postgres + pg_dump/pg_restore in CI): config
defaults/validation, full backup -> restore -> delete round-trip, and
scheduled-run retention pruning.
Also verified live against a running stack on both local-disk and MinIO
(S3) storage, including restore-from-artifact and prune.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security
- Add an otp_type discriminator to auth.email_otps and scope numeric-code
verification to NUMERIC_CODE rows, so a wrong 6-digit guess can no longer
consume a live magic-link (HASH_TOKEN) reset/verify token for the same
(email, purpose).
- Case-insensitive user lookup in signInWithOTP so a mixed-case OAuth email
no longer creates a duplicate lowercased account.
- Re-sanitize non-AppError failures in the OTP verify paths (no raw infra
error messages in 500 responses).
Refactor
- AuthOTPService.consumeNumericOTP owns the OTP transaction: it persists the
attempt counter on failure, commits caller work + consumption together, and
rolls back on caller error. signInWithOTP, verifyEmailWithCode and
exchangeCodeForToken route through it; remove the dead verifyEmailOTPWithCode.
- Resolve disableSignup via AuthConfigService; STRING_AGG(DISTINCT) providers
and drop the user_id reassignment in the email provider upsert; reuse
EMAIL_TEMPLATE_TYPES in the cloud provider.
Schema / contract / docs
- Session preprocess treats null/undefined method as the legacy password flow;
shared six-digit code schema; trim name before the length check.
- Add request-otp to the OpenAPI email-template enum; document the
unverified-account password-clearing behavior in the four REST guides.
Tests updated/added across the OTP service, route, schema, and migration suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>