Splits Storage Size into current versus noncurrent objects, and attributes the
noncurrent portion to the buckets responsible, so the cost of object versioning
sits with the metric it inflates.
The split is flag-aware. With the preview off, Storage Size renders exactly the
single real series it does today; `USAGE_CATEGORIES` takes an options argument
and only emits the two stacked segments when the preview is on.
Noncurrent is deliberately not split further into noncurrent versions and
soft-deleted files: the platform can't reliably tell the two apart after the
fact, and the user's lever for shrinking either is the same lifecycle policy.
Using S3's own terms also reads better than Studio-only words like "live".
- `storage-retention-usage-query.ts` — org-scoped (the prototype keyed this
org-level data under `['projects', undefined, …]`), real `queryOptions` shape
with the endpoint stubbed
- `StorageRetention.constants.ts` — one definition of the two segments, used by
both the chart attributes and the breakdown table, so labels, colors and key
casing cannot drift
- `StorageRetention.utils.ts` + tests — maps retention days onto chart points
- `StorageRetentionBreakdown.tsx` — the segment table, retained-data warning,
and per-bucket list, rendered through the existing `additionalInfo` hook
- `Usage.colors.ts` — `COLOR_MAP` and `AttributeColor` moved out of
`Usage.constants` so the breakdown can read a color token without a cycle
back through the module that renders it
Known limitation: with the preview on, the stacked chart and the breakdown read
zeros until the retention endpoint exists. No mock data is shipped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follows the key's move out of the component module, so the explorer state and
the preview test both address it through the same factory.
Also return the signed URL through `HttpResponse.json` with its response type,
which is what the mock-API convention asks of a success response.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Restoring a version makes it the current one, and permanently deleting it
removes it, but the widget kept offering to restore either — in the delete case
pointing at a version that is gone.
Thumbnails and the widget's older half now classify bytes by the version's own
mime type rather than the file's current one, which an overwrite can change.
That makes the history's thumbnails real images, so its test needs the bucket
they resolve through.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Overwrites an object in place, which on a versioned bucket is what creates a new
version. Not `uploadFiles`, which renames rather than overwrites on a name
clash, and with upsert set so storage does not reject the path as a conflict.
The panel's own preview URL is cached for a week against the path, so the
replace invalidates it alongside the version list; without that the panel keeps
rendering the bytes from before. A rejected write reports why where storage
answers with a bare status and no message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The redesign renamed the panel's "Get URL" button to "Copy URL" while the row
context menu kept "Get URL", leaving one action with two names, and gave the
panel's file name a `title` — the attribute the explorer rows use as their
handle, so `getByTitle` matched two elements once a preview was open.
Also snap five off-token sizes to the scale the ratchet enforces, and point the
E2E delete helper at the confirmation's real label now that it says what it
does instead of ConfirmationModal's "Submit" default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Leads with what happened and that it is permanent, then bounds the wait.
The previous wording blamed a missing schedule; the real reason there is no
exact timestamp is that the cleanup spans several systems.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
"Expiring now" claimed a moment that does not exist: cleanup is a periodic
pass, so a version that has met the policy stays listed until it runs. The
tooltip says why, within a max width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Restoring a noncurrent version invalidated the version list but left the
explorer row showing the old size, type and modified date, since the live
listing is the explorer's own state rather than a React Query cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The version history rendered a generic mime-type icon per row, and the
compare widget put that same icon on both sides — so every entry for a
file looked identical and the comparison showed nothing to compare.
The sign and public-url endpoints already accept `options.versionId`;
nothing asked for it. `useFetchFileUrlQuery` now takes a `versionId` and
keys on it, and the preview rendering moves out of `PreviewPane` into a
`FilePreview` component both the pane and the compare widget use, so a
version preview cannot silently fall back to the current bytes.
Image rows in the history list render their own thumbnail under 5MB;
larger files and other mime types keep the icon rather than pull a whole
object down for a 28px box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The file preview panel already splits the two on a versioned bucket, but
the explorer's own row menu still offered a single "Delete" — which
archives there, without saying so.
The row menu now reads "Archive" on a versioned bucket and gains a
"Delete permanently" entry beside it.
`ConfirmPurgeModal` is mounted once by the explorer and driven by
`itemToPurge` on the store, the same shape the row delete already uses.
The preview panel routes its own permanent delete through it too, so the
confirmation copy exists in one place rather than two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The preview panel now addresses versions by the object's full path, which
is what the list, move and delete endpoints take, rather than by the leaf
name the explorer renders. `VersionHistory` keeps `objectName` for copy
and takes `path` separately.
Expiry countdowns read the bucket's stored lifecycle policy instead of an
empty placeholder, so a row's fate reflects the policy that governs it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The empty state read "Overwriting this file will retain a recoverable copy here"
regardless of whether the bucket was actually versioned. Since
`getBucketVersioningState` reports `disabled` for every bucket until the API
exposes the field, that promise was showing on every file in every bucket.
Splits it: `disabled` now says versioning is off and points at the bucket
settings; `enabled`/`suspended` keep the original copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follows the same change in the version-fate helper: a version cap always arrives
alongside an expiration age, so the policy summary only has three shapes to
describe (age alone, or age plus cap under either operator). Removes the
cap-only sentence and the "no age limit" chip, and `daysRemaining` on
`expires-on-next-upload` no longer needs an undefined guard.
Also adds the explicit `tabIndex={0}` that `supabase/require-explicit-tabindex`
wants on the four raw buttons in this feature. These were lint *errors*, not
warnings, so the ratchet never surfaced them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Completes the rename in the version history rows, the lifecycle policy summary,
and the delete confirmation copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rebuilds the file preview panel around object versioning: a collapsible Versions
section listing every version with its removal outlook, an inline
compare-and-restore widget, and delete actions that say what they actually do on
a versioned bucket.
- `VersionHistory` + `VersionHistoryPolicyRow` + `VersionThumbnail` — the version
list, the inline lifecycle policy summary, and the row glyph
- `VersionCompareWidget` — takes over the top of the panel when a noncurrent
version is selected, so restoring is a visible comparison, not a modal
- `PreviewSection` — the collapsible section wrapper
- `PreviewPane` — new panel chrome, viewport-clamped thumbnail, and an
Archive / Delete permanently split button on versioned buckets
- `ConfirmDeleteModal` — on a versioned bucket a delete is a soft delete, so the
copy no longer claims it cannot be undone
Delete markers are surfaced as their own row type. They are the empty
placeholders S3 writes on a soft delete, and they can outlive the delete
(delete → upload → delete → restore leaves one mid-history), so a live file's
history can contain them. There is nothing to preview or restore, so the row is
non-interactive, dimmed, labelled "Delete marker", and its only action is
removing the marker itself.
Version data comes from the stubbed query added in the previous PR, so the
Versions section renders its empty state until the Storage API lands.
Fixes carried over from the prototype rather than ported:
- Version rows were a clickable `<li>` whose `onKeyDown` passed a function
reference instead of calling it, so keyboard activation did nothing. They are
real `<button>`s now, which fixes activation and a11y together.
- The policy summary put a `<Button>` inside tooltip content, unreachable by
pointer or keyboard — now a `HoverCard`.
- Permanent delete fired a bare `toast.success` with no mutation behind it.
- Dropped the download and "Get version URL" menu items, which were toast stubs.
- Deduplicated a double `filter` over versions and memoized the fate map.
Also re-syncs `selectedBucket` in the explorer store when the bucket query
refetches. It was only ever seeded once, so editing a bucket left consumers
reading stale metadata — which is how Copy URL could sign a URL for the wrong
visibility after a public/private toggle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The list endpoint caps at 1000 rows, so an object with a longer history had the
first page deleted and the purge reported as done, leaving the rest behind with
the row already gone from the explorer. Deleting now repeats until the history
is empty, and gives up with an error rather than spinning if a round frees
nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
A replace has to invalidate the preview URL it just made stale, and the key was
built inline at its only call site. Extracting it lets a writer address the same
entry without restating the shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
Tailwind does not know `lucide`/`lucide-broom-sparkles`, so the ratchet
counted two new `shadcn/no-unknown-classes` violations. Nothing styles or
queries them — they only mirrored what lucide-react stamps on its own icons.
Also suppress knip's unused-file report for this icon and the three version
mutations: their first consumer is the preview panel in the next PR, which
removes the entries again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
In "either condition" mode the cap is written as a separate rule pinned at
`noncurrent_days: 1`, because S3 honors a version cap only alongside an age.
A version beyond the cap was reported as expiring the moment it went
noncurrent, while nothing would touch it until it was a day old.
Also renames the fate to `expiry-due`, since removal happens on the next
cleanup pass rather than at the instant the policy is met.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
The object list endpoint now takes `noncurrentVersions`, `deleteMarkers`
and `exactMatch`, and returns `version`, `archived_at` and
`is_delete_marker` per row, so the version history has a real source.
- `objectVersionsQueryOptions` lists one object's history, with
`exactMatch` so the path isn't read as a folder prefix
- Restoring is a move onto the object's own path with `sourceVersionId`,
which consumes the source version rather than duplicating it
- Deleting one version addresses it as `{ path, versionId }`, which is a
hard delete even on a versioned bucket
- Purging reads the history first and deletes every version by id, since
a bare path would only hide whatever is current
Variables take the object's full bucket-relative `path` rather than its
leaf name, which is what these endpoints address.
`ObjectVersionAction` drops `restore`: the list endpoint has no
provenance field, so a restore is indistinguishable from an overwrite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3euXsz25sTybsGTcxCFfn
A version cap with no expiration age isn't expressible in S3 — `NoncurrentDays`
is required on any `NoncurrentVersionExpiration` rule — and the bucket form now
disables the cap until an age is set. So `computeVersionFate` no longer has a
cap-only path: no age means no policy.
That also makes `expires-on-next-upload` reachable only from the `or` branch,
where an age is always present, so `daysRemaining` is required rather than
optional.
The cap-only test block is replaced by one asserting a lone cap is ignored, so a
cap arriving without an age can never start labelling versions for removal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renames the `VersionFate` variant and its test wording to match the term used in
the form copy and the version history rows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the query and mutation hooks the version history UI needs, written to the
studio `queryOptions` conventions with the endpoint calls stubbed. No mock
fixtures: `useObjectVersionsQuery` resolves to an empty list until the Storage
API exists, so the version history renders its empty state rather than
fabricated data, and swapping in the real endpoint is a one-function change per
file.
- `object-versions-query.ts` — the version list, plus the `ObjectVersion` and
`LifecyclePolicy` types
- `object-version-restore-mutation.ts` — promote a noncurrent version to current
- `object-version-delete-mutation.ts` — remove one specific version
- `object-purge-mutation.ts` — delete an object and all of its versions,
bypassing versioning
- `VersionHistory.utils.ts` — `computeVersionFate`, the pure rule deciding what
removal outlook each version row shows
- `BroomSparklesIcon.tsx` — inline SVG for a glyph absent from lucide-react 0.436
The bucket's lifecycle policy is part of the `objectVersions` query key. The
prototype read it inside `queryFn` while leaving it out of the key, which left
stale expiry labels behind after a policy edit.
`computeVersionFate` is carried over from the prototype with its 9 reference
cases intact, minus four non-null assertions — the bounds are now narrowed
through a nullable local instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| # | Branch | Base |
| - | ------ | ---- |
| 1 | `feat/storage-versioning-private-alpha` — merged | `master` |
| 2 | `feat/storage-versioning/002-bucket-form-fields` | `master` |
| 3 | `feat/storage-versioning/003-bucket-modals` ◀ | 2 |
| 4 | `feat/storage-versioning/004-object-versions-data` | 3 |
| 5 | `feat/storage-versioning/005-file-preview-versions` | 4 |
| 6 | `feat/storage-versioning/006-billing-storage-retention` | 5 |
| 7 | `feat/storage-versioning/007-archived-objects-data` | 6 |
| 8 | `feat/storage-versioning/008-archived-rows` | 7 |
| 9 | `feat/storage-versioning/009-archived-preview-pane` | 8 |
| 10 | `feat/storage-versioning/010-replace-file` | 9 |
## [3/10] Storage object versioning: wire into the bucket modals
**Base:** `feat/storage-versioning/002-bucket-form-fields` (PR 2)
### This PR
Mounts the object-versioning form section in the create and edit bucket
modals behind the feature preview, and saves it.
- create and edit bucket modals spread `bucketVersioningFormFields` into
their existing form schema
- lifecycle defaults to 30 days / 10 versions
- edit adds a confirmation before suspending an actively versioned
bucket
## Enabling object versioning on a new bucket and setting lifecycle
policies
https://github.com/user-attachments/assets/194f8319-4929-432e-8a50-206f180a77a8
## Edit and suspend object-versioning
https://github.com/user-attachments/assets/f31e1d34-9840-4f5a-a269-6a911214742d
## To reproduce
1. Make sure storage versioning is enabled under feature previews >
Storage Versioning
2. Open Storage Bucket File explorer
3. create new bucket and enable Object Versioning
4. set lifecycle policy
- Noncurrent version expiration: can be either empty or >1
- Retained noncurrent versions: can be either empty or between 1 and 100
and can't exist without "Noncurrent version expiration"
5. Open new bucket with object versioning and test changing lifecycle
policies
6. Disabling object-versioning shows proper warning and updates
`versioning_status` to SUSPENDED (it can never go back to DISABLED once
it has been enabled on a bucket)
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
## Summary
- Bumps vulnerable transitive dependencies flagged by `pnpm audit`, one
commit per dependency (lockfile only, no permanent overrides):
proxy-addr, shell-quote, @fastify/busboy,
@graphql-tools/executor-legacy-ws, @modelcontextprotocol/sdk,
compression, http-cache-semantics, source-map-js, smol-toml, dompurify.
- Updates `scripts/fix-audit-vulnerability.ts` to be agent-friendly:
accepts a dependency name argument, adds `--json` (single JSON object on
stdout, logs on stderr, never prompts) and `--help`.
## Not fixed
The remaining audit findings could not be resolved by this script. Some
are blocked by `minimumReleaseAge` (braces, node-forge, sprintf-js);
others stay vulnerable even with an override and need a parent
dependency update or scoped override.
## Test plan
- [ ] CI passes (typecheck, lint, prettier)
- [ ] `pnpm audit` shows fewer findings than on master
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
<!-- ccr-slack-attribution -->
_Requested by **Ivan Vasilov** · [Slack
thread](https://supabase.slack.com/archives/C063LNYJJKS/p1790710416069689?thread_ts=1790710416.069689&cid=C063LNYJJKS)_
**Before:** When a Studio API GET comes back as a 200 with an empty
body, openapi-fetch hands the caller `{}` and we only see the downstream
crash, with no record of the response that caused it.
**After:** The first time this happens for an endpoint in a page
session, Studio sends one Sentry warning, `Empty response body on
successful API request`. It carries the response metadata, browser
state, resource timing, and the result of a single `cache: 'no-store'`
refetch. What the caller receives is unchanged.
## Problem
Studio crashes trace back to GET requests that return 200 with an empty
body, which openapi-fetch turns into `{}`. They are heavily skewed to
Firefox and Safari. The leading hypothesis is browser cache revalidation
(Express weak ETags, no `Cache-Control` on api.supabase.com), but
nothing confirms it yet. The `no-store` probe tells the two cases apart:
if the refetch has a body, the browser cache is the likely culprit; if
it is also empty, the server or the edge is sending empty bodies. This
data should show whether the fix belongs on the API side or the
Cloudflare side.
Context: #51041 (closed) tried to guard the crashing call sites instead.
## Needs API-side change to be fully useful
Cross-origin, Studio can only read CORS-safelisted response headers, and
resource timing sizes read as zero. If api.supabase.com sends
`Access-Control-Expose-Headers: ETag, cf-ray, cf-cache-status,
x-request-id` and `Timing-Allow-Origin: <studio origin>`, this event
will also carry the ETag, cf-ray, and cache status, plus the real
transfer and body sizes and the negotiated protocol. Until then, those
fields read as `null` or `0`.
## Solution
- `data/empty-body-diagnostics.ts` (new): `reportEmptyBodyResponse({
request, response, schemaPath })`.
- Runs only for `GET` and only when `IS_PLATFORM`. Empty POST/201 bodies
are legitimate.
- Reports at most once per templated endpoint per page session
(module-level `Set`).
- Endpoint: openapi-fetch's `schemaPath` (e.g.
`/platform/projects/{ref}/settings`), passed through
`templateEndpointPath`. That function drops the query string and hash,
replaces the segment after `projects`/`organizations`/`branches` with
`{ref}`/`{slug}`/`{branch}`, and replaces UUIDs, numeric IDs, and 20+
character alphanumeric IDs with `{id}`. I used `schemaPath` rather than
the request URL so user-chosen names (bucket names, function slugs)
never end up in tags or fingerprints.
- Probe: one plain `fetch(new Request(request, { cache: 'no-store', ...
}))` with a fresh `X-Request-Id` and a 10s `AbortController` timeout.
`AbortSignal.timeout` isn't available in older Safari. The probe
bypasses the openapi-fetch middleware, so it can't recurse. Only the
body's byte length is recorded, never its contents.
- Event: `level: 'warning'`, `fingerprint: ['empty-body-response',
endpoint]`, `tags: { endpoint, probe_has_body, empty_body_diagnostic:
'true' }`, where `probe_has_body` is `true` / `false` / `error`. `extra`
holds:
- the request: method, status, `response.type`, `redirected`, and the
original `X-Request-Id` (for API log lookup)
- response headers: `content-type`, `cache-control`, `last-modified`,
`expires`, `content-length`, `etag`, `cf-ray`, `cf-cache-status`,
`x-request-id`
- browser state: `visibilityState`, `navigator.onLine`, the navigation
type, ms since navigation start, and whether the page was restored from
bfcache
- the latest `PerformanceResourceTiming` for the URL (transfer, encoded,
and decoded size, `nextHopProtocol`, `responseStatus`)
- the probe: status, request ID, body length, `content-length`,
`content-type`, or the error name
- Fire-and-forget: everything is wrapped in a `try`/`catch`, and the
caller does not await it.
- `data/fetchers.ts`: the `onResponse` middleware passes `{ request,
schemaPath }` to `normalizeEmptyBodyResponse`, which calls the reporter
in its empty-body branch and also for a 200 that carries
`Content-Length: 0`. openapi-fetch short-circuits that case to `{}` the
same way, so it is the same symptom. The return value is unchanged in
every branch.
- `packages/common/sentry.ts`: `filterSentryEvent` normally keeps only
1% of events that aren't page crashes. It now sends events tagged
`empty_body_diagnostic` unsampled, with `codeSampleRate: '1'`. A
once-per-session warning would barely show up at 1%. Consent and
platform gating and the third-party filter still apply. www and docs
also use `filterSentryEvent`, but only Studio's reporter sets this tag,
so sampling for them and for every other Studio event is unchanged.
Sentry config: Studio's `beforeSend` doesn't otherwise drop this
message. It has no exception values, so the no-stack-trace filter
doesn't apply, and it matches no `ignoreErrors` entry.
## Review instructions
1. Check `normalizeEmptyBodyResponse` in `data/fetchers.ts`: the
reporter is `void`-called and its return value is untouched.
2. Check `probe()` in `data/empty-body-diagnostics.ts`: only
`byteLength` is read from the body. The probe reuses the original
request's headers and credentials (same auth as the original GET).
3. Check `filterSentryEvent` in `packages/common/sentry.ts`: only the
`empty_body_diagnostic` tag skips sampling.
4. Tests: `data/empty-body-diagnostics.test.ts` and
`packages/common/sentry.test.ts`.
## Verification
- Unit tests (`data/empty-body-diagnostics.test.ts`, new):
- path templating cases
- `probe_has_body` `true` / `false` / `error`
- the secret body content never appears in the Sentry call
- one report per endpoint
- non-GET and non-platform requests are skipped
- no throw when `fetch` or Sentry throws
- end-to-end through `client.GET`: still resolves `{}` and reports the
`schemaPath`, for both a missing `Content-Length` and `Content-Length:
0`
- `packages/common/sentry.test.ts`: tagged diagnostics are sent
unsampled, untagged or false-tagged ones are still sampled, and they're
still dropped without consent.
These tests, plus the existing `normalizeEmptyBodyResponse.test.ts`,
`handleError.test.ts`, and the rest of `sentry.test.ts`, pass (67 tests)
under vitest 5 + jsdom. I ran them in a minimal harness, not the full
`pnpm install` workspace, because the local checkout is sparse.
- I ran TypeScript 7.0.2 (`--strict`) on the five touched files against
the real `api-types`, with stubbed `common`/Sentry types. No errors in
the touched files.
- Prettier `--check` with the repo config passes, with and without
`SORT_IMPORTS=false`.
- Not run locally: the full studio typecheck, `lint:ratchet`, and knip.
CI covers them. The change adds no `any`, no default exports, and no
deps.
## Checklist
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01UefDak8XYLMi9aiEjDPXc5
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Problem
`parseCronJobCommand` reads the timeout of an `net.http_get` /
`net.http_post` cron command like this:
```ts
const timeout = timeoutMatch?.[1] || ''
// ...
timeoutMs: Number(timeout ?? 1000),
```
When the command has no `timeout_milliseconds` argument, `timeout` is
`''`, which isn't nullish, so the `?? 1000` fallback never applies and
`timeoutMs` becomes `Number('') === 0`.
`timeout_milliseconds` is optional in pg_net, so this is common for jobs
created in SQL, e.g.:
```sql
select cron.schedule('ping', '* * * * *', $$ select net.http_get(url:='https://example.com/health') $$);
```
Opening such a job in the cron editor shows a timeout of **0 ms**, and
saving fails validation (the field requires 1000 to 5000 ms) until the
user edits a value they never set.
## Solution
Use the parsed value when present, and otherwise fall back to pg_net's
default for `timeout_milliseconds` (5000 ms in current pg_net). That's
what the job actually runs with, so opening and saving it in Studio
doesn't change its behavior. It's also within the form's allowed range.
Added tests for the Edge Function and HTTP request paths without
`timeout_milliseconds`. Both fail on `master` (`timeoutMs: 0`), and the
existing cron tests still pass.
## Review instructions
1. In the SQL editor, run:
```sql
select cron.schedule('ping', '0 * * * *', $$ select
net.http_get(url:='https://example.com') $$);
```
2. Open Integrations > Cron > `ping` > Edit.
3. Before: the timeout field shows `0` and saving shows a validation
error. After: it shows `5000` and saves.
4. `pnpm --filter studio test
components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts`
## Checklist
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)
## Problem
We want to be able to show free-plan organizations a simplified region
selector that only lists general regions (Americas, Europe,
Asia-Pacific), controlled per organization through ConfigCat.
## Solution
- ConfigCat flags are now evaluated with `organization_slug` and
`organization_created_at` (Unix seconds) custom attributes, so flags can
target and bucket by organization.
- `organization_created_at` is read from `GET
/platform/organizations/{slug}`, fetched only for free-plan
organizations, since the organization list response doesn't include it.
- Two flags:
- `freeTierGeneralRegionEnrollment`: the organization is enrolled
(control or test).
- `freeTierGeneralRegionSelection`: the organization sees only general
regions.
- For enrolled free organizations, the region selector stays in its
loading state until flags have been evaluated with the organization's
creation time, so specific regions aren't shown and then removed.
- In the test variant, the selector hides specific regions and shows a
footer linking to the plan upgrade panel. High Availability keeps its
own region list.
- Telemetry: new `free_tier_general_region_experiment_exposed` and
`free_tier_general_region_upgrade_clicked` events, and
`freeTierGeneralRegionExperiment` / `regionSelectionType` properties on
`project_creation_simple_version_submitted`.
- `created_at` is added to `OrganizationSlugResponse` in the generated
platform types, matching the API.
## Review instructions
1. With both flags off, open `/new/[slug]` for a free organization and
confirm the region selector is unchanged.
2. Using the dev toolbar, set `freeTierGeneralRegionEnrollment` and
`freeTierGeneralRegionSelection` to `true`. Confirm only general regions
are listed and the footer links to the billing plan panel.
3. Set `freeTierGeneralRegionSelection` to `false` and confirm the full
selector is shown.
4. Repeat with a paid organization and confirm the full selector is
always shown.
## Problem
The model selection logic in `generate-v4.ts` was incorrectly ignoring
the requested model even when it was available in the base model set.
The condition checked for access to advanced models or throttling in a
way that would force fallback to the default base model even when the
requested model was already a base model.
## Solution
Reordered the logical condition to first check if the effective model is
a base model ID, and only apply the access/throttle checks when the
requested model is not already a base model. This ensures the requested
model is respected when it's available.
Also threaded the `effectiveModel` parameter through to the assistant
response generator for better telemetry and debugging context.
## Review instructions
1. Check the logic change in
`apps/studio/pages/api/ai/sql/generate-v4.ts` line 164
2. Verify the condition now correctly checks
`isAssistantBaseModelId(effectiveModel)` first
3. Confirm `effectiveModel` is now passed to `generateAssistantResponse`
for proper telemetry
## Checklist
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Running Studio locally without the incident.io env vars produces a 500
from `/api/status-page` on every fetch, plus a server-side log and a
client `console.error`. Retries and the 5-minute refetch keep repeating
them, so the dev console stays noisy.
**Changed:**
- `lib/api/status-page.ts` – when `INCIDENT_IO_WIDGET_URL` is unset
outside production, return an empty (degraded, short-cached) status page
instead of throwing. The missing API key / status page ID log is also
limited to production. Production behavior is unchanged.
- `data/platform/status-page-query.ts` – remove the client-side
`console.error`, which repeated the error that React Query already
exposes
**Added:**
- Tests for the missing widget URL in dev (empty payload, no error log)
and in production (still throws)
## To test
- Run Studio locally in platform mode without `INCIDENT_IO_*` set: no
status page errors in the terminal or the browser console, and
`/api/status-page` returns 200 with empty arrays
- Support form and project creation still render normally (no status
banner, status page link points to status.supabase.com)
Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
The scoped OAuth consent interstitial: scope display, org/project
selection,
consent screen assembly, success screen, and post-submit role-validation
error state. Consolidates the former stacked PRs #49481, #49484, #49486,
#49489, #49951 into one reviewable unit (each contained the last; no
code
was dropped).
- Entirely behind `useFlag('oauthAppScopedGrants')` — no reachable UI
with
the flag off.
- Runs on the mock data layer from #49476 (`USE_MOCKS`-gated); no real
endpoints are called.
- Covers PROD-626, PROD-627, PROD-628, PROD-629, PROD-653.
Base is #49476 (data contracts + mocks); will retarget to master once it
merges.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
## Problem
Auth Providers uses a bespoke Enabled/Disabled chip that mixes brand
tokens with primary colour. Custom Auth Providers already uses the
standard `Badge` for the same meaning.
## Solution
Replace the bespoke chip with the existing `Badge`:
```tsx
<Badge variant={isActive ? 'success' : 'default'}>
{isActive ? 'Enabled' : 'Disabled'}
</Badge>
```
No new status primitive in this PR. Built-in providers now match Custom
Providers.
| Before | After |
| --- | --- |
| <img width="958" height="1568" alt="CleanShot 2026-10-06 at 17 19
40@2x"
src="https://github.com/user-attachments/assets/08850f4e-528a-4e47-958f-207e72783b16"
/> | <img width="934" height="1482" alt="CleanShot 2026-10-07 at 11 46
41@2x"
src="https://github.com/user-attachments/assets/232903f7-86bd-4c5c-ae1d-d873cddcf67b"
/> |
## Review instructions
1. Open Auth → Providers (built-in list). Confirm each provider shows a
small `Enabled` / `Disabled` Badge, with no brand/primary token mash-up
or oversized pill.
2. Optional: open Auth → Providers → Custom and compare the Enabled
column; it should feel like the same Badge treatment.
## Checklist
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)
## Problem
Joshen left a copy nit on #51302 after merge: the dialog intro reads
awkwardly as two short lines.
## Solution
Merge them into one sentence: “We’ve updated our Terms of Service which
includes the following:”
## Review instructions
1. Open an organisation landing page (`/organizations` or `/org/<slug>`)
in the Studio preview.
2. Click **Learn more** on the Terms of Service notice.
3. Confirm the dialog opens with a single intro line before the bullet
list, not two separate sentences.
## Checklist
Check all before review:
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)
## Problem
The design-system Sidebar page was mostly an upstream shadcn paste:
first-person voice, a broken `/blocks` link, missing structure images,
CLI install steps that do not match this monorepo, and a long changelog
/ data-fetching tutorial that do not apply here.
Separately, it still taught classic shadcn **HSL channel** variables
plus `hsl(var(--sidebar-*))`. In this monorepo those tokens are **full
colours**. Mixing the two patterns produces invalid CSS.
Related call-site cleanup:
https://github.com/supabase/supabase/pull/51161
## Solution
- Rewrite the Sidebar docs as a shorter monorepo guide: import from
`'ui'`, structure, theming, provider / sidebar props, menu building
blocks, controlled mode, state styling.
- Move Studio’s `--sidebar-*` aliases into shared `packages/ui` compat
CSS so every app on the shared theme gets working `bg-sidebar`
utilities.
- Drop the duplicate definitions from Studio `globals.css`.
Left alone on purpose: brand / destructive channel tokens and docs that
correctly use `hsl(var(--brand-…))`.
## Review instructions
Design-system preview:
[design-system](https://design-system-git-dnywh-docssidebar-full-colour-tokens-supabase.vercel.app/)
1. [Live Sidebar
docs](https://supabase.com/design-system/docs/components/sidebar) ·
[Preview Sidebar
docs](https://design-system-git-dnywh-docssidebar-full-colour-tokens-supabase.vercel.app/design-system/docs/components/sidebar).
Confirm the page is no longer the upstream essay: no broken images, no
`/blocks` link, imports from `'ui'`, theming shows full-colour aliases.
2. Smoke Studio: left nav should look unchanged (same aliases, now from
compat.css).
3. Optional: in DevTools, confirm `--sidebar-background` resolves to a
full `oklch(...)` colour.
## Checklist
- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)