# Changelog

All notable changes to sbarbase. The format follows Keep a Changelog and the
project uses Semantic Versioning, which for a 0.x version means the interface may
still change between releases.

The declared package version is 0.5.13. Historical 0.1.0 and 0.2.0 entries retain their original scope. Later Docker and VM rehearsals are dated in [status](https://base.sbarah.com/en/docs/reference/status/); no independent public-server or production release acceptance is established.

## [Unreleased]

### Still open

- **Deleting an environment does not work on an ordinary installation.** It always answers `409` with "Exact ownership inventory unavailable" and changes nothing, so restore and purge cannot be reached either. Provisioning never records the exact list of an environment's resources, and the shared PostgreSQL cluster and Storage process have no check yet that would make such a list honest. The [API reference](https://base.sbarah.com/en/docs/reference/api/) said delete "can answer" `409`; it now says it always does, and a test pins the current behaviour.

## [0.5.13] - 2026-10-19

### Fixed

- Signing out of the console no longer ends your access tokens. The console also signs out on its own when a session expires, so an agent's MCP or `/v1` access could stop with `401` while nobody did anything. Tokens now end only as the agents guide says: when revoked or expired, at a password reset or factor recovery, or when the MFA policy changes. A token already stopped this way stays stopped: create a new one.

## [0.5.12] - 2026-10-19

### Fixed

- A Studio opened before the upgrade to 0.5.11 also starts again by itself after it: on that first restart the activity record does not exist yet, so the time the console last asked for the session is used.

## [0.5.11] - 2026-10-19

### Fixed

- **No sign-in flash.** With a stored sign-in, the console waits on a neutral screen while the Auth client checks it, instead of showing the sign-in page for a moment and then the console. With no stored sign-in, the sign-in page shows at once.
- **Studio stays open while it is used.** The Studio session renews itself while its tab makes requests. It ends after eight hours without any request or seven days after it was opened from the console; signing out, a password change or losing the role still end it at once ([Studio](https://base.sbarah.com/en/docs/guides/studio/)).
- **Studio comes back after a restart.** A restart or an upgrade brings back a Studio used in the last eight hours, after the upgrade is confirmed. An open tab shows "Studio is starting", reloads by itself and continues.

## [0.5.10] - 2026-10-19

### Fixed

- **Docker installs can set a public Studio address.** `compose.yaml`, `deploy/compose.sh` and the host preflight now forward `SBARBASE_STUDIO_HOST`; before, the console container could never receive it ([Studio](https://base.sbarah.com/en/docs/guides/studio/)).

## [0.5.9] - 2026-10-19

### Fixed

- **Studio's SQL editor and Edge Functions page.** Saved SQL snippets now live in a folder inside the Studio session instead of failing with "SNIPPETS_MANAGEMENT_FOLDER env var is not set", and the Edge Functions page lists each function's deployed code, copied when Studio opens.
- **Studio can change what migrations made.** Studio works as the environment's developer login, as Supabase Studio works as a project's `postgres` user: it can alter tables, policies and row level security, and sees the developer's own schemas, Vault and migrations. What it creates belongs to the same owner ([Studio](https://base.sbarah.com/en/docs/guides/studio/)).
- **A blocked Studio route is caught.** Opening Studio checks its route to Auth and Storage from inside Studio, not from the host, and names the firewall when it is blocked. The server guide lists the two narrow firewall rules it needs ([server deployment](https://base.sbarah.com/en/docs/guides/server-deployment/#firewall-two-routes-from-containers-to-the-host)).

## [0.5.8] - 2026-10-19

### Added

- **One setting for Studio's public address.** `lab/studio_address.py plan DOMAIN` lists what to provide once (a wildcard DNS record, a certificate that covers it and the setting); `apply DOMAIN` writes `SBARBASE_STUDIO_HOST` for the console and the TLS proxy and restarts them; `verify DOMAIN` says which step is missing. The TLS proxy now reads `SBARBASE_STUDIO_HOST` when it has no `--studio-host` flag, so one drop-in configures both. After that, every new project opens Studio with no DNS or certificate work ([Studio](https://base.sbarah.com/en/docs/guides/studio/)).

### Changed

- The console's notice for a server without a public Studio address now names this setup instead of only the variable.

### Added

- **An environment can create its own schemas and use Vault.** The developer login, used by direct database access and by agents, may now create and rename schemas of its own, so migrations that keep code outside `public` (for example `create schema app`) run as on Supabase, and so does `supabase db push`. Names the platform uses (`auth`, `storage`, `extensions`, `realtime`, `vault`, `cron`, `net`, `graphql`, and any starting with `supabase` or `sbarbase`) stay refused, by a guard the login can neither drop nor disable. Vault is installed in each environment with the grants Supabase gives `postgres` and `service_role`, without the right to pass them on; all environments of a server share its root key. The `supabase_realtime` publication now always exists and belongs to the login, so migrations can add tables to it as on Supabase. Extensions that reach the server's network or see every environment, such as `http`, `pg_net`, `dblink`, `postgres_fdw`, `pg_stat_statements` and `pgroonga` (which can write files anywhere on the server), are now refused for the environment's login; the image's supautils used to create them for it. Turn database access off and on once for an environment whose access is already on ([database access](https://base.sbarah.com/en/docs/guides/database-access/)).

### Fixed

- **Turning database access off and on keeps a migration's narrower default grants.** Each time access was turned on, the API's default grants to `anon`, `authenticated` and `service_role` were given again, undoing a migration that had revoked them, for example execute rights on new functions. They are now given once, the first time ([database access](https://base.sbarah.com/en/docs/guides/database-access/)).

### Changed

- **The console has a new look.** Colours, spacing and components follow Supabase's design tokens, each environment screen has one primary action, and a contrast check guards the palette (pull request 18).

## [0.5.7] - 2026-10-19

### Fixed

- **Studio opens behind the TLS proxy.** Studio compresses its pages. The console's Studio proxy and the TLS proxy both let Bun's `fetch` decode the body while keeping the `content-encoding` header, so the next hop received plain bytes labelled gzip and failed with a decompression error (Cloudflare showed 520). Both now pass Studio's bytes and header through together.

## [0.5.6] - 2026-10-19

### Fixed

- **No more sign-outs after an hour away.** The console's access token lasts an hour. A request made with an expired one signed you out and deleted the session on the server, although the session could have been renewed. The console now renews the token and retries once, and signs out only if the renewal is refused.

## [0.5.5] - 2026-10-19

### Fixed

- **No more Studio tab that never loads.** On a server whose Studio has no public address, opening the dashboard from the console's public address left a tab reading "Starting Supabase Studio" forever. The console now learns this from the Studio status before opening anything, shows the SSH tunnel steps and opens no tab; a tab already opened for a new project says what happened and closes ([Studio](https://base.sbarah.com/en/docs/guides/studio/)).

### Changed

- **The console looks and reads like Supabase.** Every page now uses Supabase's own colour tokens and the look of its buttons, fields, cards, alerts, badges and tables, with stronger edges so sections, fields and buttons no longer blend into one grey. The main action on a screen is the one solid green button, read-only values such as tokens and commands look different from fields you type into, a new key or token appears in a green-edged panel, and focus rings are visible in both themes. Text and control edges meet WCAG AA in light and dark, checked by `tests/ui-contrast.test.ts`. Routes, labels and behaviour are unchanged ([console design](https://base.sbarah.com/en/docs/reference/console-design/)).
- **The console keeps you signed in for thirty days.** `SBARBASE_MANAGEMENT_SESSION_SECONDS` now defaults to 2592000 instead of 43200, so a sign-in no longer ends after twelve hours. Set a lower value to keep the old behaviour ([configuration](https://base.sbarah.com/en/docs/reference/configuration/)).
- **Telegram notifications are in plain words.** A message says what happened, where (by name), the likely cause and what to do, without ids, field names or reason codes. Email, the webhook and the catalog keep the full record ([OPERATOR-NOTIFICATIONS](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/OPERATOR-NOTIFICATIONS.md), section 3.6).

## [0.5.4] - 2026-10-18

### Changed

- **Install with Docker is the front door.** The Docker guide starts from a bare Ubuntu 26.04 machine: `docker.io`, `docker-compose-v2` and `docker-buildx` from the distribution, then clone, check, up and bootstrap, with a note to wait for the console before bootstrap ([Install with Docker](https://base.sbarah.com/en/docs/guides/docker/)). The website's install section shows these five commands; the systemd quickstart stays linked as "Without Docker". `lab/vm-docker-rehearsal.sh` ran it in a local virtual machine, 14 checks ([vm-docker-install-checks.json](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/vm-docker-install-checks.json)); not yet on a real server.

### Security

- **Edge Functions leave the shared runtime network.** Each environment's Functions container reaches only its own Auth and REST on that environment's network, and Storage through a host listener that pins the environment's tenant, so one environment's functions cannot reach another's services ([FUNCTIONS-NETWORK-ISOLATION](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/FUNCTIONS-NETWORK-ISOLATION.md)). The outbound network refuses traffic between its containers.

## [0.5.3] - 2026-10-17

### Added

- **Studio from the public address.** `SBARBASE_STUDIO_HOST` sets each environment's Studio address as a pattern, such as `studio-{id}.example.com`, and the TLS proxy serves it with `--studio-host`. With a wildcard DNS record and a certificate that covers it, the console opens Studio on that address instead of `<id>.studio.localhost`, which only an SSH tunnel reaches ([Studio](https://base.sbarah.com/en/docs/guides/studio/#open-it-from-anywhere)). Until then, the console opened from its public address explains the tunnel instead of opening a tab the browser cannot reach.

### Security

- The TLS proxy serves only its public host, loopback names and matching Studio addresses, and answers any other host with 421, so a wildcard DNS record never shows the console under another name.
- On a public address, Studio's session cookie is `__Host-` and `Secure`; a Studio entry ticket works once; a request that changes something must come from the Studio page itself; a request another site starts is refused, except a plain visit, which lands on the project home without its query.

### Fixed

- Studio containers no longer show as unhealthy while they serve: the image's own health check asked an address Studio does not listen on.
- Notices on the Functions, Sign-in, Database, Environments and Updates pages kept their original look; a style added in 0.4.0 had changed it.

## [0.5.2] - 2026-10-17

### Fixed

- A table an agent created through the MCP endpoint or `/v1` answered 404 to the app until PostgREST reloaded its schema some other way. After a migration or a query that may change data, the server now tells PostgREST to reload, as Supabase's database does. Found by an agent's live deploy on the real server.

## [0.5.1] - 2026-10-17

### Fixed

- **An Edge Function's answer could not be read by most clients.** The gateway passed the caller's `Accept-Encoding` to the Edge runtime, which compressed its answer; the gateway's own fetch then unpacked it but kept the `content-encoding: gzip` header, so `fetch` and supabase-js failed on the body. Upstream services are now asked for uncompressed answers; the TLS proxy or CDN compresses for the client. Found by an agent's live deploy on the real server.
- `supabase functions deploy --use-api` now lands the same layout as a console deploy: the CLI's `supabase/functions/<name>/...` and `supabase/functions/_shared/...` paths become the function's own files and its shared code.
- An access token's name may now hold parentheses, apostrophes and `@ : / +`.

## [0.5.0] - 2026-10-17

### Added

- **AI agents can deploy to Sbarbase.** A person creates a personal access token (`sbp_...`) on the console's new **Access tokens** page: scoped to everything they can reach, one organization or one project, read only or not, expiring within a year. With it, an agent such as Claude Code, Cursor or Codex connects to `https://<console>/mcp`, which runs Supabase's own MCP server (the official `@supabase/mcp-server-supabase`). It can create projects, run SQL and migrations, deploy Edge Functions, read keys, logs and security advisors ([AI agents](https://base.sbarah.com/en/docs/guides/agents/)).
- **A Supabase-compatible management API** under `/v1`, for the same tokens: organizations, projects (each environment is a project, with a 20-letter ref), API keys, SQL, migration history, Edge Functions and their secrets, logs and advisors.
- **A Claude Code skill** (`deploy/agent-skill/sbarbase/SKILL.md`) that teaches an agent what "deploy to Sbarbase" means.
- **Publishable keys can be copied again** from the API keys page, as on Supabase, since they are public by design. This applies to keys created from this version; secret keys are still kept only as a hash and shown once.

### Changed

- The developer login now keeps migration history in a `supabase_migrations` schema made for it. It still cannot create schemas. Turn database access off and on once for an environment whose access was already on.

### Fixed

- The database access guide said `supabase db push --db-url` works with the developer login. It never did: the CLI's `CREATE SCHEMA IF NOT EXISTS` needs a right the login does not have, on purpose. The guide now says so and shows `psql` instead.

## [0.4.0] - 2026-10-17

### Added

- **Secret API keys**, as on Supabase. On an environment's **API keys** page, or `POST /management/v1/environments/{id}/keys` with `{"type":"secret","name":"backend"}`, create a named `sb_secret_` key. Behind the gateway it acts as `service_role` for REST, Auth admin, Storage, Realtime and Edge Functions; a signed-in user's token still wins. A request that looks like a browser's is refused with it. Like publishable keys, it is kept only as a hash and shown once; creating and revoking keys is audited ([API keys](https://base.sbarah.com/en/docs/guides/api-keys/)).

## [0.3.2] - 2026-10-17

### Added

- **`sbarbase reset-password <email>`** sets a new password for a management account from the server's shell, for an operator who lost it. The new password is asked twice without echo; every console and Studio session of that account ends; its authenticator stays; the change is audited ([operator setup](https://base.sbarah.com/en/docs/guides/operator-setup/#a-forgotten-password)).

### Fixed

- The capability registry still named version 0.2.0 in 0.3.0 and 0.3.1, so its verifier refused them as a version mismatch. `lab/release.py prepare` now writes the version there too.

## [0.3.1] - 2026-10-17

### Fixed

- **0.3.0 could not be installed through the update channel.** Its manifest declared the Auth and Storage schema migrations as data migrations, which puts a release outside the channel entirely. Those migrations are already detected from the pin changes and need the operator's confirmation (`attended`); 0.3.1 declares none and carries the same code as 0.3.0. The one-time database restart and the way back limit above four environments are in its notes.

## [0.3.0] - 2026-10-17

Moving from 0.2.0 migrates the Auth and Storage schemas of every environment, so the way back is a restore of the backup the upgrade takes first. The first start raises the database's connection limit and restarts it once. Nothing here is claimed production ready.

### Added

- **Up to eight environments per installation** (four before). The runtime raises PostgreSQL's `max_connections` from 100 to 200 on start, the database container may run 320 processes, and the gateway admits 64 requests in flight (8 per environment). A synthetic drill of eight environment databases under full load on the 1 GiB, one CPU database tier peaked at 613 MiB ([evidence](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/evidence/capacity-drill-2026-10-16.json), `lab/capacity-drill.sh`). On a 4 core server, eight fit only without Realtime or Edge Functions. Deleting an environment still does not free its place.
- **Telegram notifications connected from the console.** An installation owner pastes the bot token on the new Notifications page; it is checked with Telegram, kept 0600 on the server and never shown again, and the chat is bound by a one-time `/start` link. Admins see the state, only owners change it, every change is audited, and a new chat does not receive the old backlog. The notification drain rereads its configuration, so a change applies without a restart.
- **Alerts for low disk space and stopped services.** Under 10 % or 5 GiB free is a warning, under 5 % or 2 GiB critical; the database, Storage, the management sign-in or an environment's Auth, REST or Realtime container stopped for five minutes is critical. Each is reported at most once an hour.
- **An outside uptime check** (`deploy/uptime-worker/`): a Cloudflare Worker that asks `/health` every five minutes and sends one Telegram message after two failures and one on recovery.
- **The upstream currency work** ([plan](https://github.com/M7MMAD-OMAR/sbarbase/blob/main/docs/engineering/plans/2026-10-14-upstream-currency.md)): upstream drift tracking; an attended move of the database onto another PostgreSQL image (`lab/upgrade.py database`); the database volume as state; rebuilding the whole cluster from one backup run (`lab/cluster_rebuild.py`) and taking it into use (`lab/cluster_takeover.py`); a per-environment write pause; backup and restore of the management database; starting the database alone for a restore after a failed Storage way back.
- **Updated upstream services:** Auth v2.197.0, PostgREST v14.18, Storage v1.83.3, Realtime v2.144.0, Edge Runtime v1.77.4, Studio 2026.10.12 and postgres-meta v0.100.0. PostgreSQL stays 17.6; 17.11 waits for its rehearsal.

### Fixed

- **Studio and postgres-meta are no longer reachable from other environments.** Neither checks its callers, and both sat on the shared runtime network, where another environment's Edge Functions or the database's `pg_net` could run SQL as the Studio login while a session ran. Each session now has its own network, postgres-meta listens on loopback inside Studio's namespace, and every start checks that nothing answers on the shared network.
- **Restore on a server installed under umask 002.** The runtime state directory was left 775 and every restore refused its journal; each creator of it now makes it 0700.

### Verified on the real server

- Upgrades from 0.2.0 through the update command with the new images, confirmed by the health checks.
- A restore of an environment from its local backup, and a restore served only from the encrypted off-site copy in Cloudflare R2; the data came back exactly as backed up.
- Studio confinement probed from a container on the shared network before and after the fix.

### Still open

- A cluster takeover whose own replacement never becomes ready has no exit (S4); its fourth part is not accepted.
- A restore onto a new server from the off-site copy, an interrupted restore, and a whole installation under real traffic have not been exercised.
- A way back to an older version refuses jobs once more than four environments exist.

## [0.2.0] - 2026-09-26, and changes until 0.3.0

These entries were listed as unreleased; they cover 0.2.0 and what was merged after its tag until 0.3.0.

### Added

- Current product and portability roadmap, Docker-only verification contract, explicit native-placement identity and scoped evidence ledger. Native activation, complete recovery and release acceptance remain open.
- Bounded native-effects verification now preserves raw ACL distinctions and enforces phase deadlines. The latest actual fixture still refuses its routine privilege model; this is verification work, not accepted native-effects operation.
- Updated current documentation and repository layout, preserving historical results and distinguishing implemented shared-runtime features from experimental native placement.

- **Rename, move and delete clients, projects and environments.** Owners and
  admins rename; owners move a project to another client they own, which
  revokes its API keys, and delete what is empty. An environment is deleted
  only while nothing runs or changes for it; its keys stop working at once
  (the gateway answers 401), and its data stays on the server. From the
  console or the management API.
- **Restore a backup onto a new installation.** `sbarbase relink <runtime>
  <backup>` recreates the backup's client, project and environment with their
  original ids, never matching by name, and `sbarbase restore` then fills it.
  Members, API keys and the signing key do not travel; users sign in again
  with their old passwords. Rehearsed onto a second VM on 2026-09-25.
- **Rotate an environment's signing key.** Owners and admins press **Rotate
  signing key** (or `POST .../signing-key/rotate`); the supervisor gives the
  environment a new JWT secret, recreates its Auth and REST, updates its
  Storage and Realtime tenants and Edge Functions, and ends every session.
  Publishable keys keep working. A rotation that stops halfway is finished by
  the next attempt or start. Also: a restart no longer drops an environment's
  direct database access from the published state.
- **Move a project from Supabase in one command.** `lab/import_project.py`
  reads a Supabase project (Cloud, self-hosted, or another environment) and
  copies it into a new, empty environment: the `public` schema with its
  policies and the source's own grants, users with their password hashes,
  every row, buckets and files with their owners, and triggers on
  `auth.users` and `storage.objects`, then compares counts before reporting
  success. `--dry-run` only inspects.
- **Encrypted off-site backup copies.** `lab/offsite.py configure` takes S3
  settings (Cloudflare R2, Backblaze B2, AWS S3, MinIO) and a passphrase on
  stdin; from then on each daily backup is encrypted on the server
  (AES-256-GCM in chunks) and copied to the bucket, the newest 30 per
  environment are kept, and `fetch` brings one back for `backup.py restore`.
- **Direct database access per environment.** Owners and admins turn on a
  developer login and get a PostgreSQL connection string, with the password
  shown once, for `psql`, `pg_dump`, `supabase db push --db-url`, Prisma or
  Drizzle over an SSH tunnel. The login works like a project's `postgres`
  user within its own database, including triggers on `auth.users` and
  Storage policies, but is not a superuser. A loopback listener (port 6543)
  passes on only developer logins to their own database.
- **Edge Functions per environment.** Deploy a Supabase project's
  `supabase/functions` folder with one command (`lab/functions-deploy.ts`),
  `_shared` and `verify_jwt` in `config.toml` included, or write a function in
  the console. Each environment that deploys one gets its own pinned upstream
  edge-runtime; functions are called through the gateway with
  `supabase.functions.invoke()`, get `SUPABASE_URL` and the anon and service
  role keys for their own environment, read secrets set in the console, reach
  the internet for `npm:` imports, and can take keyless webhooks when
  `verify_jwt` is off.
- **Uploads up to 50 MiB, or the limit you set.** File uploads to Storage are
  passed through the gateway and the TLS proxy as they arrive instead of being
  capped at 1 MiB. `SBARBASE_UPLOAD_LIMIT_MB` (50 by default, as on Supabase)
  sets the limit for the gateway, the proxy and Storage together; a change
  applies at the next start by recreating the shared Storage container. Other
  API bodies stay at 1 MiB.
- **Logs and metrics per environment.** Every member sees the last hour of
  requests through the gateway on the environment's page: totals, client and
  server errors, median and 95th percentile response times, requests per minute
  and per service, and the memory and processor use of its own services. Owners
  and admins also read the last requests and the Auth, REST, Storage and
  Realtime logs, with keys, tokens and passwords redacted before a line leaves
  the server. Counts live in memory and start again after a restart.
- **Realtime per environment.** Owners and admins turn Realtime on for an
  environment from the console; it runs the pinned upstream Realtime for that
  environment alone, with its own login, and stops when turned off. supabase-js
  broadcast, presence and database changes (every `public` table, filtered by
  row level security) work through the gateway, which checks the key before any
  socket reaches Realtime. The login holds administrator rights only while
  Realtime creates its own schema, first start and after an upgrade.
- **Sign-in settings and OAuth providers per environment.** Owners and admins
  set the site URL, allowed redirect addresses, sign-up and anonymous sign-in,
  and any of 19 providers (Google, GitHub, Apple and others) from the console.
  Saving recreates only that environment's Auth, keeping the previous one if the
  new one does not start. Email links and OAuth steps reach Auth without a key,
  as on Supabase. `SBARBASE_PUBLIC_URL` sets the address links and callbacks use.
- **Browser access.** A page on any domain can call Auth, REST and Storage
  through the gateway, as on Supabase: every answer carries
  `Access-Control-Allow-Origin: *`, a preflight gets 204 without a key, and no
  credentials mode is offered. The operator login stays same-origin.
- **Upgrades with an automatic way back.** `lab/upgrade.py start` checks the new
  version, pulls its images and backs up every environment, then moves the
  checkout; the restart replaces only the Auth, REST and Storage containers whose
  pin or configuration changed. If that start fails, the supervisor moves back to
  the previous version by itself. A version that changes the PostgreSQL image is
  refused. CI upgrades PostgREST v14.15 to v14.16, then recovers from a broken
  version, on a clean machine.
- **Fair share admission at the gateway.** Each environment is guaranteed 8
  requests in flight; a busy one borrows idle slots up to 24 of 32, while the
  unused share of every environment active in the last minute (at least 8 slots)
  stays free. Checked over loopback HTTP (`docs/evidence/fair-share-checks.json`),
  not against Supabase or under sustained load.
- **Saturation notice.** After 15 minutes in a row of refusals at its limit, or of
  crowding out a neighbour, an environment raises one `environment.saturated`
  operator notification. Borrowing alone never does.
- **Telegram** as a third notification channel beside email and webhook.
- **Invitations.** Owners and admins invite someone by email and role; the
  console shows a one-time link that lasts 7 days. The invitee chooses a password
  and joins, or signs in first if they already have an account. Unit tested with
  a fake management Auth, not yet against the live one.
- **Member roles in the console.** An owner changes a member's role or removes
  them; access ends at the next request. The last owner stays.
- **Activity.** Owners and admins see what happened in their organization,
  newest first, in the console (`GET /organizations/{id}/audit`). A project
  moved in from another client shows only what happened since it arrived.
- **A gateway share per environment.** The console's environment page shows each
  environment's guaranteed share. Installation operators raise or lower it and
  see the installation's allocation; a client organization sees only its own
  share. The gateway applies a change at the next request, and a raise that
  would exceed the gateway's 32 is refused.
- **Supabase Studio per environment.** Owners and admins start the original,
  pinned Studio and postgres-meta for one environment from the console, open it
  on its own address behind the console login, and stop it. Studio signs in with
  a per-start login that is not a superuser and exists only while it runs. CI
  uses tables, SQL, users and buckets through it on a clean machine.
- **Install with Docker.** `docker compose up -d --build` on any Linux host with
  Docker: the image carries Python 3.14, Bun and the Docker CLI, and starts the
  pinned Supabase services as sibling containers. CI runs build, first operator,
  first project through supabase-js, restart and a clean stop on a clean machine.
- **Empty-server rehearsal in a local VM.** `lab/vm-rehearsal.sh` boots a
  disposable Fedora 44 Cloud VM, installs from a clean clone with the one-command
  acceptance, creates a first project and reboots. Passed on 4 vCPU and 6 GiB
  (`docs/evidence/vm-empty-server-rehearsal.json`). Not a real server.
- **First project check.** `lab/first-project-check.ts`, and `--first-project` on
  `deploy/server-acceptance.sh`: login, project, environment, key, supabase-js
  Auth, REST and Storage through the gateway, and a refused revoked key.
- `SBARBASE_CONSOLE_PORT` pins the console's loopback port for a TLS proxy.
- CI for the unit suites, with a manual empty-host acceptance job. The Python
  suite also runs against an unreachable Docker endpoint, so a unit test that
  needs the daemon fails in review.
- **Import inspection (phase 0).** `lab/import_inspect.py` reads a Supabase
  source read-only and reports what an import would refuse, warn about or leave
  manual (`docs/evidence/import-inspect-checks.json`). The import itself does not
  exist yet.
- Management API: create an organization (owners and admins of the bootstrap
  organization), list an organization's members, retry a failed or cancelled
  environment; the console has matching screens.
- The control catalog records its schema version and refuses one written by a
  newer release.
- A recovery export records the owning organization, project and environment.
- Documentation restructured into explain, guides, reference and decisions, with
  hand-drawn diagrams, a quickstart, a server guide, `SECURITY.md`, a threat
  model, `CONTRIBUTING.md` and a roadmap.

- **Container generation migration.** `lab/migrate-generation.py` replaces a
  managed database container on the same data volume under an fsynced intent
  record, one checkpoint per phase and an explicit `--reconcile`. Five SIGKILL
  crash points pass on the disposable fixture
  (`docs/evidence/fresh-worker-generation-crash-all.json`). It refuses the
  retained placement: that run is a deliberate attended step and has not happened,
  so `lab/durable-check.ts` stays disabled and the two blocked load measurements
  stay blocked.

### Fixed

- Found by the milestone runs in the rehearsal VM on 2026-09-25: redeeming an
  invitation answered 500, because the account calls went to `/auth/v1/...` on
  the management Auth service itself, which serves them at its root; the first
  upgrade after an acceptance was refused as local changes, because the
  acceptance rewrites tracked evidence (it is now copied aside to
  `.lab/upgrades/`); the acceptance waited for the console right after
  installing the unit, before the images existed, so an empty host always
  timed out (the unit now starts after the installation); and an image pull
  gave up after one immediate retry (now four attempts, waiting 15, 45 and 90 s).
- A backup counted its rows before `pg_dump` ran, so a sign-up in between put a
  row in the dump that the manifest did not count, and the restore refused the
  backup. The counts and the dump now read one snapshot.
- The loopback listener announced `connection: close` after refusing a request
  but kept the connection open; a pooled client reused it and a one-second
  cleanup cut off the next long request. The sustained arrival run now passes.
- Found by the empty-server rehearsal: the acceptance checked the console before
  building it; block IO limits named a partition, which the kernel rejects, so the
  database never started on a usual VPS disk layout; body-less POST and DELETE
  requests got a body over the real listener, so key issuance and revocation
  answered 400; image pulls ran under a 600 second timeout with no progress; a
  failed first launch was reported as a retained source to adopt.
- An owner or admin of any organization could read every organization's
  notifications, including their ids and failure reasons. Each caller now sees
  only their own organizations' events.
- CI was red on a host whose root filesystem is a partition: a test still
  expected the partition where the runtime correctly names the whole disk.
- Five admission unit tests called Docker for real and failed without a daemon.
- The server acceptance recorded the unit as active in the same second it
  started it. After every start it now waits, at most 300 seconds, until the
  console answers over loopback, and fails with the last thing it saw.

### Changed

- The control catalog schema is now version 3: the delivery table accepts the
  Telegram channel. The migration is one way; an older release refuses a catalog
  this release has opened.
- The application gateway lends idle slots instead of refusing every request
  above 8 per environment; the overload probe keeps measuring the fixed share.
- The start requirement is derived from the placement a start runs (1792 MiB of
  limits on an empty server, plus 512 MiB per environment, plus the 2560 MiB
  reserve), and the preflight, the unit's `ExecStartPre` and the runtime use one
  computation. It was a fixed 5888 MiB in the preflight and 6 GiB in the runtime.
- Every GitHub Actions step is pinned to a full commit SHA, with the tag it was
  resolved from as a comment, so a moved tag cannot change what CI runs.
- CPU limits are admitted as ceilings: up to twice the cores after one core for
  the host. The old rule asked for 8 cores.
- A new environment is refused when the next restart could not admit it.
  On an installation that moved an environment, that check and the preflight
  now count the current recovery target's containers in one computation; the
  check used to count the source placement only, and a running target's memory
  is now counted as in use rather than twice. A restored target that was never
  cut over is counted by neither, because the next start does not run it.
- Project names are unique within an organization and environment names within
  a project; a clash answers 409, not 500. The API refuses an environment past
  the installation limit with 409 before queueing it.
- Moving a project cancels its queued jobs in the move and points its jobs at the
  destination, so later events name the new owners.

- The two load vehicles no longer overwrite their own evidence on failure or let
  cleanup replace the real error, and the SDK vehicle probes its fixture first.
- `NOTICE` no longer describes Supabase Studio as served; it is planned.

### Notes

- A `LICENSE` file (Apache-2.0) is present in the repository. The 0.1.0 entry
  below says there is none; that entry is kept as it was released.

## [0.1.0] - 2026-09-21

The first tagged milestone. It carries the platform's isolation and accounting
work, per environment mail, operator notifications, and the measurement evidence
that says what the isolation does and does not do.

### Added

- **Resource tiers and block IO separation.** Every container is launched under a
  policy tier with a CPU weight, a block IO weight and per device read and write
  bandwidth and IOPS limits. The device is resolved from the host rather than
  written down, and a launch refuses rather than starting a container with no
  block IO separation. `lab/resource_policy.py`, `docs/engineering/RESOURCE-POLICY.md`.
- **A counted placement derived from the daemon.** Admission reads the containers
  the daemon attributes to the owner label instead of a hand written name list, so
  a component nobody told the module about is still counted, and a labelled
  container outside the recorded placement is a refusal while it runs.
- **Continuous host pressure sampling and a level 1 response.**
  `lab/pressure_admission.py` takes a bounded series over a window, summarises it,
  and refuses new admissions while the most recent reading is at or over a
  threshold, appending every crossing to a durable ledger. Every decision carries
  what is not implemented next to the action.
- **Per environment mail.** One 0600 configuration file per environment, an Auth
  environment built from it, an explicit reconcile command that recreates one
  environment's Auth to apply or remove a configuration, and a non secret state
  file. `lab/mail_config.py`, `lab/mail_state.py`, `docs/engineering/ENVIRONMENT-EMAIL.md`.
- **Operator notifications.** A durable outbox and delivery tables whose rows
  commit inside the transaction that records the state change they describe, a
  drain inside the worker, an email channel and a signed webhook channel, a
  fail-closed credential redaction gate, and eleven event kinds wired at their
  durable state changes (installation started, stopped and failed start, worker
  restart and restart limit, fence applied and released, export completed and
  failed, restore verified and failed). `lab/notify.py`,
  `lab/notification_producers.py`, `docs/engineering/OPERATOR-NOTIFICATIONS.md`.
- **Console surfaces for both.** A read only route for an environment's mail state
  and one for the undelivered notification count, with the mail state on the
  environment surface and the count in the console shell.
- **Disposable probes with negative controls.** A mail probe that proves delivery,
  an unconfigured environment, a dead relay and a wrong credential, and a
  notification probe that proves delivery, deduplication, the redaction gate and
  that a broken channel records a failure instead of changing the operation it
  reports.
- **Documentation.** Three design records, an adversarial review that states the
  unfakeable acceptance criteria, an execution plan, a plan for the deferred
  container generation migration, and the measurement evidence under
  `docs/evidence/`.
- Per environment upstream Studio presented on the administration path, and the
  console following the operating system theme by default.

### Changed

- Placement arithmetic is derived rather than listed, and the block IO rows are
  the mechanism that binds, because the weight column was measured not to.
- The unwritten `environment_mail` catalog table was removed in favour of the
  runtime's own summary file, which is the single source the console reads.
- The mail configuration comparison runs in both directions, so a key the desired
  configuration no longer has cannot survive in a reused container.

### Fixed

- Mail: the unchanged path recorded a state the vocabulary does not define, so a
  second run against an unchanged configuration raised instead of reporting. A
  source level test now walks every record call site and requires a defined state.
- Mail: a configuration an operator deleted could survive a restart and keep a
  live SMTP credential, rate limits and relay cooldown in the reused container.
- Notifications: the escalation window was the severity window, so a broken channel
  would have escalated about 72 times per six hours instead of once, and two broken
  channels could escalate each other.
- Placement: an owned container outside the recorded placement refused installation
  startup where the earlier revision admitted, and a labelled container under any
  other owner escaped the count while the module's own documentation claimed it was
  counted.
- Placement: every recovery target creation site now carries its tier label, and
  the guard that claimed every launch site passes a tier is a floor rather than an
  exact count that a new launch site breaks.
- Notification probe: its default run now exercises the fail-closed redaction gate,
  which it did not, and its summary comparison no longer re-reads a render time
  sentence that made one check a coin flip.
- Mail: a dangling symlink is refused instead of silently reading as unconfigured.

### Measured, with the evidence committed

- `docs/evidence/resource-policy-cgroup-mapping.json`: `--cpu-shares` maps to the
  cgroup v2 `cpu.weight` sublinearly (2048 asks for 174, not 800), `--blkio-weight`
  did not bind at all on this host, and `--device-write-bps` did, landing a numeric
  `wbps` in `io.max`.
- `docs/evidence/noisy-neighbor-sql-runs.json`: five runs of the neighbour
  experiment, three preserved. No repeatable effect: the median, the p95 and the
  maximum stay flat between the baseline and the phase where a second environment
  saturates the shared engine. One earlier run showed a tail spike that four later
  runs did not reproduce, and it is recorded as an outlier rather than as the
  neighbour penalty.
- `docs/evidence/pressure-response-checks.json`: a real threshold crossing on a
  disposable container, the level 1 refusal, one durable ledger line, and a return
  to admitting after recovery.
- `docs/evidence/auth-templates-source-v2.196.0.json`: what a template variable of
  the pinned Auth version does, read from the tag's own source with a digest per
  file. A template value is an HTTP URL, never a file.
- Probes re-run by the maintainer on the landed tree: mail 36 checks, notifications
  21 checks, both cleaning up what they created. Gates: 575 Python tests, 87 Bun
  tests, UI typecheck, console build check.

### Not in this release

Stated here rather than left to be discovered:

- **No server rehearsal.** The deployment readiness matrix is implemented and
  proven on a workstation, and it is not a rehearsal against real hardware.
- **The container generation migration is deferred**, so a retained database
  container cannot be recreated. The tier contract therefore applies to placements
  created after it, and the retained placement on a development host stays
  grandfathered. `docs/engineering/CONTAINER-GENERATION-MIGRATION.md` is the plan.
- **Two measurements are blocked, not merely unrun**: the arrival driven pressure
  experiment and the mixed SDK load. Their only fixture generator,
  `lab/durable-check.ts`, is disabled pending that migration, and its fixture on a
  development host names a retired environment. One load vehicle also overwrites
  its own evidence when it fails, and its cleanup replaces the real error.
- **The tier tables are uncalibrated.** The block IO rows bind and separate the
  tiers, and no experiment has measured what a tier receives under load.
- **Three notification kinds are emitted by nobody**: pressure, runtime refused and
  headroom changed. Nothing calls the pressure response on a schedule.
- **Mail is read only in the console** and the operator still chooses an SMTP
  provider. Templates are the upstream defaults, and an override has to be served
  from inside the runtime network.
- **No licence file.** `NOTICE` states the copyright and the upstream attributions,
  including that Supabase Studio and postgres-meta are used as unmodified pinned
  images and that the console is sbarbase's own work. Choosing a licence is the
  maintainer's decision and is not made here.