Local sandbox using server-owned corpuses¶
Implementation is available through npm run dev:sandbox. This document records
the design; the sandbox guide describes the delivered commands,
including password resets. The verification items below are the design's test
plan, not a claim that every path has been exercised on every runtime.
The design targets the corpus foundation merged in server PR #411. Corpus generation, maintenance, and restore verification belong to the server repository.
Decision and initial target¶
Use the server's comprehensive backup as the first sandbox corpus. The frontend
launcher selects a server image, downloads the corpus from that image's exact
source revision, restores it into an isolated disposable database, and starts
host-side Next.js with the resulting connection settings.
The existing hubuum-admin --verify-backup and --restore commands provide the
loading interface. Published images need no server checkout or Rust toolchain.
Commits and PRs without published images can use an isolated source checkout and
the server's Dockerfile; compilation then runs inside the container build.
Initial target:
| Item | Value |
|---|---|
| Server source revision | 5baa9008cce9929b123624067266d3fe221eeb69 |
| Published image tag | ghcr.io/hubuum/hubuum-server:sha-5baa9008cce9929b123624067266d3fe221eeb69 |
| Image index digest | sha256:54b96bbd12b8aa476c0ea3e922940c58793bda5b67678edacff01852e2f91b6d |
| Verified platforms | linux/amd64, linux/arm64 |
| Corpus | comprehensive |
| Recipe revision | 2 |
| Backup format | 6 |
| Backup bytes | 25,206,572 |
| Backup SHA-256 | 0fba27a0688f074018bf81d723f96331009b3bc70f9625a54eb7e3b99314510e |
On 2026-09-13, the merge CI run completed successfully and the image was confirmed published. Registry checks verified the index digest and both platform images' source-revision labels against the commit above. Pin the full image reference for integration runs:
ghcr.io/hubuum/hubuum-server@sha256:54b96bbd12b8aa476c0ea3e922940c58793bda5b67678edacff01852e2f91b6d
The matching files are the backup, manifest, and recipe. The server corpus guide defines the scenarios and maintenance contract.
This selects a development sandbox target. The released frontend compatibility target remains governed by compatibility.
Selecting a tag, commit, PR, or image digest¶
Accept exactly one target selector for a fresh sandbox:
| Selector | Meaning | Resolution |
|---|---|---|
--tag main or --tag vX.Y.Z |
Published Hubuum Server image tag | Pull the tag once, pin its registry digest, and read its source revision |
--sha 5baa9008 |
Full or abbreviated Git commit SHA | Resolve to a unique full commit, then use sha-<full-commit> or build that commit locally |
--pr 411 |
PR number in hubuum/hubuum |
Resolve the PR's selected commit, then use its published SHA image or build locally |
--pr https://github.com/hubuum/hubuum/pull/411 |
The same PR by URL | Validate the host and repository, then resolve as above |
--image ghcr.io/hubuum/hubuum-server@sha256:<digest> |
Exact published image | Pull that digest and read its source revision |
Keep Git commit SHAs distinct from container image digests. --sha accepts
7–40 hexadecimal characters, verifies the returned full SHA matches that prefix,
and rejects unknown or ambiguous prefixes. --image takes the full registry
reference with its SHA-256 digest. Conflicting selectors fail before network or
container work. Tags refer to registry tags, not arbitrary Git branches.
For an open PR, select head.sha. A closed, unmerged PR also selects its last
head when the source remains available. For a merged PR, select the actual
merge_commit_sha, which includes the result of squash/rebase merging as
reported by GitHub. Do not use an open PR's synthetic test-merge commit.
For example, --pr 411 resolves to 5baa9008cce9929b123624067266d3fe221eeb69;
use --sha 898c757cd44262506bc0943ec69b1f94e73e96e5 to reproduce its original
head instead. Show PR state and the selected full SHA before setup begins.
Resolve mutable inputs once per fresh run and freeze the resulting commit.
Capture the original selector, PR number/state, head repository when relevant,
full commit, platform, image identity, and corpus identity in sandbox state.
A force-push, later merge, or moving tag must not change a run already resolving
or provisioning. resume always uses the recorded image and database. A fresh
run resolves again; --sha or --image reproduces an earlier choice explicitly.
Use GitHub's API to resolve PR metadata and abbreviated commits. Missing source,
API rate limits, and authorization failures return actionable errors; do not
silently select main. GitHub credentials stay in the resolver process and must
not be forwarded into the server build or runtime containers.
When no published image exists¶
The server's
PR container check
builds with push: false; the
main publishing job
publishes sha-<commit> images. PR selection therefore needs a local-build path.
For --sha and --pr, first look for the exact commit's published image. On a
confirmed missing manifest, use a matching cached local build or build the exact
commit. --no-build disables this fallback for callers that require a published
image. Registry timeouts, authentication failures, or a source-label mismatch
are errors, not evidence that an image is missing. Tag and digest selectors
require their requested published image; they never substitute source builds.
Build from a clean, detached checkout in the sandbox's ignored source cache, without changing either developer checkout. For fork PRs, fetch through the canonical repository's PR head ref and verify that it yields the full SHA pinned from PR metadata. If the ref moved, retrieve the pinned commit or fail explicitly. Read the corpus, recipe, and manifest from that same verified source tree; a missing or incompatible corpus requires a server-side update, not frontend generation or substitution from a different revision. Check artifact presence and checksum metadata before the expensive build; run the selected image's offline backup verifier after the build completes.
Use that commit's Dockerfile with the production feature flags, locked Cargo
dependencies, and HUBUUM_BUILD_GIT_SHA=<full-commit>. Set source/revision labels
explicitly and record the actual source and fork provenance. The build context
contains only the selected tracked source snapshot; do not add .env.local,
host credentials, SSH agents, or host sockets, or run repository scripts on the
host. Report build progress and allow cancellation with cleanup of owned work.
The container build requires network access and can take substantially longer
than pulling an image.
Cache local builds by full commit, platform, Dockerfile identity, and build
arguments. Base images and locked dependencies still affect rebuild identity;
record the resulting image ID rather than claiming a source SHA determines
identical image bytes. Use only the launcher-recorded cache entry whose labels
match. A local image may have no registry RepoDigest: retain its immutable
local image ID, use it for every role with pulling disabled, and record that the
image was locally built. resume requires that exact image to remain present;
it must not silently rebuild a replacement. Runtime configuration and the BFF
boundary remain the same for published and locally built targets.
Image and corpus matching¶
The launcher must resolve the server and corpus as one unit:
- Resolve the selector and obtain its image as above. Record the registry digest
or local image ID and platform. Read
org.opencontainers.image.revisionfrom that image and require the full SHA to match the selected source revision. Validate source provenance against the repository selected by the resolver. - Obtain
test-corpora/<name>.manifest.json, its backup, and the recipe from that exact commit, by download or from the verified detached source tree. Never fetch the corpus independently from a movingmainbranch. Initially support onlycomprehensive; unknown names fail explicitly. - Validate the manifest shape, name, recipe revision and recipe checksum, backup byte count, and backup SHA-256. Bound all downloads; the current corpus has a 25 MiB ceiling. Use atomic cache writes and reject incomplete files.
- Run the matching image's offline backup verification before allocating the database or restoring anything. Invalid artifacts prevent startup.
- Record the image identity, source revision, corpus name, recipe revision, and artifact checksum together in the sandbox state and verification output.
Use fixed upstream repository URLs. Treat manifest content as data, never shell
arguments or executable instructions. Cache artifacts outside Git under ignored
.local/, keyed by source revision, name, and checksum. Recheck cached bytes.
The backup's source_version is its producing package version; it is not a
sufficient compatibility key. In this manifest it says 0.0.14 even though the
backup is format 6 and the released v0.0.14 server uses format 5.
The server's guarantee comes from testing the committed artifact against the server at the containing revision, plus independent regeneration and restore verification. An artifact can remain unchanged across compatible server commits. Do not require regeneration solely because the server commit changed.
Older images or commits without a matching corpus fail with an actionable unsupported-target error. Published images must have a source-revision label; local builds must have the provenance recorded by the launcher. There is no fallback to the newest corpus or acceptance of unrelated local image tags.
Initial corpus and later coverage¶
comprehensive contains 3,000 live objects in twelve classes, with four classes
per schema policy:
| Policy | Classes and object counts |
|---|---|
| No schema | untyped-empty (0), untyped-notes (100), untyped-locations (300), untyped-assets (600) |
| Defined, enforcement disabled | advisory-servers (100), advisory-switches (200), advisory-services (300), advisory-applications (400) |
| Enforced | enforced-servers (100), enforced-switches (200), enforced-services (300), enforced-applications (400) |
It also contains five collections including the system root, three class
relations, 300 object relations, 45 shared computed definitions, and one personal
definition owned by corpus-reader. Scenarios cover inherited permissions,
hidden relation endpoints, updated and deleted object history, and retired,
active, and staged schema revisions. The manifest supplies stable anchor IDs.
The named accounts are corpus-admin, corpus-editor, corpus-reader, and
corpus-outsider; the standard admin account also exists. Editor and reader
can see 1,600 objects while the restricted collection contains the other 1,400.
Backups exclude password hashes and bearer tokens, so credentials are provisioned
after every fresh restore.
The graph has 100 four-object components with maximum degree three and 2,600
isolated objects. It covers basic relation and visibility behavior. A later
server-owned relation corpus should add high-degree anchors, deeper paths,
dense components, and larger relation lists. Other useful follow-ups are a small
smoke corpus, more than 250 classes/collections for selector pagination,
duplicate names across collection paths, and executable report templates.
The current corpus has no export templates or external integration records.
These are follow-up corpus proposals, not supported names in the first launcher.
The existing server large and huge scale profiles remain separate benchmark
inputs until the server publishes them through a supported corpus distribution
path. Keep large artifacts and generation code in the server's ownership.
Frontend command and lifecycle¶
Proposed npm interface:
# Select a published tag, a Git commit, or a PR.
npm run dev:sandbox -- --tag main --corpus comprehensive
npm run dev:sandbox -- --sha 5baa9008
npm run dev:sandbox -- --pr 411
npm run dev:sandbox -- --pr https://github.com/hubuum/hubuum/pull/411
# Require a published image for this commit; disable the local-build fallback.
npm run dev:sandbox -- --sha 5baa9008 --no-build
# Retain this run, then resume its exact image and edited data later.
npm run dev:sandbox -- --tag main --name review --keep
npm run dev:sandbox -- resume --name review
# Inspect or remove the named sandbox's owned resources.
npm run dev:sandbox -- status --name review
npm run dev:sandbox -- down --name review
Require exactly one of --tag, --sha, --pr, or --image for a fresh run;
default the corpus to comprehensive. There is no generation seed option:
repeatability comes from restoring the exact artifact. Fresh runs resolve mutable
targets again; resume uses the recorded image identity and existing database.
Use a dedicated compose.sandbox.yml and Node launcher alongside the existing
scripts. Reuse established runtime detection, readiness checks, and frontend
port/listen options. Preserve the Valkey-only dev:deps and externally configured
dev workflows. The stack owns PostgreSQL, one-shot administrator jobs, Hubuum,
the restore executor, and dedicated Valkey. All server roles use the same registry
digest or immutable local image ID.
Startup order:
- Resolve and verify the image and corpus as above. Check the frontend port and
checkout lock before allocating services. Create a project/run ID owned by
this checkout, with state under
.local/sandboxes/<name>/. An existing name requiresresumeordown; never replace it implicitly. - Start private PostgreSQL and Valkey. Wait for health and migrate the new database with the selected image. Keep API and worker processes stopped.
- Mount the verified backup read-only in a one-shot administrator container and
run
--restore <path> --restore-confirmation "REPLACE ALL HUBUUM DATA"against that owned database. This uses offline CLI restore; no web restore capability or asynchronous confirmation flow is needed during initial provisioning. - Start the matching server and restore executor. Wait for
/readyz, then provision login credentials and wait for schema revalidation and computed rebuilding to satisfy the corpus's documented checks. - Launch
scripts/dev-server.mjswith explicit server-onlyBACKEND_BASE_URL,VALKEY_URL, and per-run session/settings prefixes in its child environment. Browser requests retain normal BFF routing and cookie authentication. - Print the frontend/backend URLs, image identity, corpus identity, verified
counts, and cleanup command. Ctrl-C stops the owned frontend and removes the
stack and volumes unless
--keepwas supplied. A failed stage prevents frontend startup; retain failed containers only when explicitly requested.
Bind published services to IPv4 loopback. PostgreSQL does not need a published
port. Prefer dynamically allocated backend and Valkey ports discovered through
Compose; forward existing --port and --listen frontend options. Never kill an
existing process to free a port. Use shell-free child-process arguments and
deadlines for every stage. Timeout and subprocess errors must redact credentials
as carefully as ordinary nonzero exits.
Keep .env.local unchanged. Override inherited backend/session connection values
with the verified sandbox values, and never accept a remote database or backend
URL for restoration. The state file contains identities, ports, and owned
resource references, with no passwords, tokens, or restore capabilities.
down checks ownership labels and removes exact recorded resources; it does not
prune globally or delete a project solely because its name matches. On Linux it
can also stop a frontend left behind by an exited launcher, after verifying its
checkout, per-run prefixes, launcher command, and dedicated process group. It
waits for that frontend to exit before removing dependencies. A live launcher
still owns its shutdown through Ctrl-C. resume
requires the recorded image and database; missing state must not silently reseed.
Each fresh run gets a new database. Changing server versions must not implicitly migrate or downgrade a retained database. Upgrade testing is a separate workflow. Multiple backend stacks can coexist, but initially run one Next.js instance per checkout because development output and locking are shared. Parallel frontend comparisons require separate checkouts and browser contexts: cookies are scoped by host, not port. Device-local browser preferences are outside the corpus.
Login and readiness¶
Capture hubuum-admin --reset-password admin output in memory and use the issued
credential for temporary administrative API access. For interactive development,
prompt without echo for a developer-chosen local password and set it on the
selected corpus account through the existing user-update API, preserving its
concurrency requirements. Offer the same provisioning for other corpus accounts
when needed. Authenticate through the ordinary login page afterward. Revoke the
bootstrap token and discard temporary credentials when provisioning finishes.
For authenticated Playwright tests, reset admin immediately before the run and
supply the generated credential only to the test child as E2E_PASSWORD, with
E2E_USERNAME=admin. Never print or persist generated passwords, and never put
passwords or tokens in command arguments. Backend bearer tokens remain on the
server side of the frontend's BFF boundary. Resume retains existing password
hashes and requires no corpus reload.
Readiness needs both healthy dependencies and completed restore follow-up work. Poll the documented enforced-class counts and computed examples with bounded timeouts; retain history by keeping event retention purging disabled. Verify stable anchors and expected counts before reporting the sandbox ready. Keep the full, mutating scenario suite as an explicit test run: it edits restored objects and must not run automatically before an everyday development session.
Delivery and verification¶
The frontend tooling provides argument parsing, artifact matching/cache
validation, owned container lifecycle, login provisioning, and the Next.js
wrapper. It adds no corpus generation or backup-format conversion. Consider a
server-owned machine-readable corpus catalogue when multiple corpuses are
published; the first version can use the known comprehensive file convention.
Test selector conflicts, ambiguous/unknown SHAs, invalid PR URLs, open/closed/merged
PR resolution, fork heads, force-push races, and moving-tag snapshot behavior.
Cover missing published images versus registry/authentication failures,
--no-build, local build cancellation, stale build caches, local images without
registry digests, and resume after an image is removed. Also test
image/corpus mismatch, missing corpus artifacts, absent source labels,
unsupported corpus names, corrupted/truncated downloads, cached checksums,
environment precedence, occupied ports, failed migrations/restores/readiness,
credential redaction including timeout paths, interrupted startup, keep/resume,
stale state, and cleanup that preserves unrelated services.
Run the real stack against the published merge image, check normal BFF login, multi-page object browsing, schema-policy display, computed values, relations, and restricted-reader permissions. Record the image digest and corpus checksum in test output, and measure startup time and resource use before setting budgets. Exercise both a published commit image and a locally built PR commit using the same corpus assertions; the local path must not depend on the developer's server checkout or host Rust toolchain.