Every branch deserved a URL
Building disposable preview environments made review concrete and turned duplicated build work into a solvable systems problem.
Code review changed when the reviewer could open the idea.
Before R7K1, a branch existed as a diff and a set of instructions. A reviewer who wanted to feel a layout change had to fetch the branch, install the right dependencies, reproduce environment variables, run migrations, and hope the local machine behaved like the author's. Small interface decisions received comments on source because executing them was disproportionately expensive.
R7K1 gave each branch an isolated URL. That description was simple enough to fit in a sentence and broad enough to occupy the better part of a year.
The local setup document had become a review tax
The project R7K1 previewed was intentionally ordinary: a server-rendered application, a small database, assets built from JavaScript and CSS, and several environment variables. That ordinariness made the setup cost more revealing.
Review instructions included a runtime version, package installation, fixture import, hostname entry, and a warning not to point the branch at shared data. Somebody interested in one interaction could spend twenty minutes reconstructing the environment and five minutes reviewing it.
The cost distorted which feedback arrived. Engineers comfortable with the stack reviewed behavior. Everyone else reviewed screenshots or waited until a shared environment changed after merge. Responsive, keyboard, content, and transition details were often first experienced after the decision was expensive to reverse.
I defined R7K1 around one handoff:
source proposal -> stable review URL -> observable candidate -> informed feedbackThe URL was not the whole product. It was the reduction in ceremony that made unfinished work available to the people whose judgment could improve it.
The first success criterion was not deployment speed. It was whether a reviewer with no local repository could identify the candidate and complete a realistic task.
A preview was a state machine
The first prototype responded to a repository webhook by running a shell script. The script cloned the branch, built a Docker image, started a container, and wrote a reverse-proxy rule. When it worked, the result felt magical. When it failed, it left partial containers, stale routes, and no useful explanation.
We replaced the script with explicit preview states:
requested → building → ready → expiring → removed
↘ failedEach transition had one owner and a durable record. The controller could resume cleanup after a restart. A failed build retained logs but never received a live route. Updating a branch created a new immutable build; the URL moved only after its health check passed.
That last decision borrowed from M31V's atomic releases. A preview could be disposable without being allowed to flicker between half-built states.
The state machine grew beyond the simplified public diagram. A durable request captured the branch at one commit. A verified artifact could exist before an environment. An environment could be running but not ready. A ready environment could wait behind another generation at the branch route.
request -> artifact -> environment -> readiness -> route activationEach arrow produced a receipt. That chain let R7K1 recover from a controller restart and explain whether failure belonged to source, build, provisioning, application startup, or routing.
The branch page summarized the chain without exposing the machinery as one giant progress spinner. A reviewer could see that the current generation remained available while the next build failed.
This was the point where the project stopped being a webhook script and became a small control plane.
One request became one generation
Repository events arrived quickly and sometimes out of order. R7K1 resolved the branch to a commit at request time and assigned a generation under the review item.
Later pushes created later generations. Completion order did not decide which one became current. Before route activation, the controller checked that the generation was still the newest eligible proposal or had been selected explicitly.
An older slow build could finish and populate the artifact store without replacing the URL for a newer candidate. A deleted branch began expiry but did not erase logs or a pinned comparison immediately.
This produced a useful timeline:
generation 12 · commit A · ready · previously active
generation 13 · commit B · failed readiness
generation 14 · commit C · activeThe branch remained a moving doorway. Generation identity made review evidence stable enough to compare and discuss.
Containers gave us a consistent unit for the application and its dependencies. They did not decide how much CPU an untrusted branch deserved, how long an abandoned preview should live, which secrets it could access, or what happened when two builds wanted the same port.
R7K1 ran on a small pool of hosts with a simple queue. Each build received resource limits and a temporary workspace. Preview containers joined an isolated network and used synthetic data. Production credentials were never present in the build environment. Outbound access was restricted to the package sources the build genuinely needed.
We treated branch names as display labels, not safe identifiers. Routes used generated slugs, and every value crossing into a command or configuration file was validated. A preview system executes code proposed by contributors; convenience does not make that code trusted.
The lifecycle policy mattered just as much. Previews attached to closed changes expired quickly. Active previews received a deadline that could be extended. Cleanup was idempotent because it often ran after a partial failure and needed to tolerate resources that were already gone.
Synthetic data was part of the interface
Early previews started with an empty database. They were isolated and nearly impossible to review. Product states such as long labels, validation errors, permissions, and partial history did not appear unless every reviewer recreated them manually.
R7K1 introduced versioned synthetic fixtures. A base fixture represented normal navigation, while named scenarios covered empty, crowded, delayed, and permission-limited states. The preview header exposed which fixture the environment used.
Fixtures contained no production copy and used generated identities. Their schema version joined the environment receipt, so an application expecting a newer database could not quietly run against an older seed.
Reviewers could reset a disposable fixture to its original state or select another declared scenario. Reset was an environment operation with progress and completion, not a page button that hoped a database script finished.
Synthetic data improved privacy and review quality together. It also exposed fixture maintenance as product work. A feature that changed the domain often needed a new representative scenario before it could be reviewed meaningfully.
Building a branch executed package scripts and compiler tooling. Running it executed the proposed application. Both handled unaccepted code and needed limited reach.
Build workers used temporary workspaces, bounded resources, and a network policy that allowed only required dependency sources. They received no production credentials. Artifact upload used a narrowly scoped identity after the build completed.
Runtime environments joined a separate preview network, received synthetic-data credentials specific to one database, and could reach only declared sandbox dependencies. The routing layer was outside their control.
This separation prevented a build script from inheriting runtime secrets and prevented the application from reaching the build controller merely because both used containers.
R7K1 did not claim perfect hostile-code isolation. It reduced capability, recorded exceptions, and treated contributor code according to its actual acceptance state. Convenience did not convert a branch into trusted infrastructure.
The same work arrived under different names
The queue became slow as adoption grew. Logs showed that many branches rebuilt identical dependency layers and sometimes identical application output. Docker's layer cache helped when a build landed on the same host with a friendly instruction order, but host-local luck was not a strategy.
We created a normalized build manifest containing:
- The source commit.
- The dependency lockfile digest.
- The build recipe version.
- The selected base image digest.
- The non-secret build configuration.
Artifacts were stored by the manifest digest. A request first checked for a completed artifact with the same inputs. Concurrent requests for the same digest joined one build instead of starting several. A failed artifact was never cached as success, and changing the build recipe invalidated reuse even when source stayed the same.
This was the project's main technical breakthrough. The important unit was not the branch. It was the normalized set of inputs that determined output.
The manifest also improved diagnosis. A build could say which lockfile, recipe, base image, and public configuration produced it. Two branches pointing at the same commit could reuse output when every declared input matched. The same branch commit could rebuild when a base image or recipe changed.
Artifact reuse and environment reuse remained separate. The same immutable image could start with another fixture or security profile and produce a distinct environment receipt.
Concurrent build requests used a lease around the manifest digest. Followers joined the active attempt and received its result. If the builder disappeared, a later worker reconciled registry evidence before taking over. It did not assume absence of a final log meant absence of an artifact.
The cache became safe because reuse was a protocol with identity and verification, not a directory named after a branch.
There were limits. Some builds embedded timestamps or generated nondeterministic asset names, so identical inputs did not always produce identical bits. The manifest still made reuse safe because it pointed to one completed artifact rather than asserting reproducibility we had not earned. We later removed several nondeterministic steps, but R7K1 did not require perfection before providing value.
URLs changed the review itself
The largest outcome was social rather than computational. Product and design colleagues could review a real behavior without learning the repository. A link could be attached to a specific conversation. Two alternatives could exist at once. Accessibility and responsive checks happened before merge because the work was available in the browser where it would live.
The URL also made unfinished work more legible. The preview page showed the commit, build time, expiry, and whether its data was synthetic. Nobody had to guess whether they were looking at the latest push.
Review comments became more specific. “This code seems risky” could become “keyboard focus is lost after the panel closes.” The diff remained essential for understanding implementation, but it was no longer asked to stand in for experience.
We made one early mistake by rebuilding immediately on every push. A quick sequence of commits filled the queue with work nobody would view. A short debounce window and cancellation of superseded queued builds reduced waste without delaying deliberate reviews. Builds already executing were allowed to finish if another request could reuse their artifact.
The preview URL needed to communicate more than the application under test. A narrow header showed branch label, commit, generation, readiness, synthetic-data notice, and expiry. Details linked build logs, affected test results, and previous generations.
The header lived outside the application artifact where possible. A raw-candidate view allowed testing layout and framing behavior without the toolbar. Review access policy covered both.
If a new generation was building, the branch page kept serving the current ready environment and displayed its status. If no generation had ever become ready, it showed a progress or failure page rather than routing to a half-started container.
The page also made staleness visible. A reviewer could compare the commit in the code review with the commit in the preview before leaving feedback.
This interface turned R7K1's infrastructure model into reviewer confidence. Without it, durable identifiers would remain hidden in logs while the URL continued to feel ambiguous.
Readiness followed a user task
The first health check proved only that the application port accepted a connection. R7K1 then activated a preview whose database migration was incomplete and whose main route returned a maintenance page.
Readiness became layered. The process reported artifact identity and schema compatibility. The controller requested the application through its planned hostname. A smoke task loaded the main route, followed one navigation, and checked a representative response marker.
The exact smoke path belonged to the previewed project and changed through versioned configuration. R7K1's controller could distinguish process liveness, application initialization, and review readiness.
The route moved only after the complete check. After activation, ongoing liveness could trigger bounded restart or removal without redefining the original build as invalid.
“Container running” was useful operational state. “Reviewer can exercise the candidate” was the product gate.
The queue was a product surface
As more branches gained previews, builds waited. A hidden queue made the system feel broken and encouraged repeated webhook retries that added more work.
R7K1 showed queue position approximately, reason for delay, requested generation, and whether a reusable build already existed. It avoided fake minute estimates from insufficient history.
Admission control protected ready environments from build bursts. Build workers had separate capacity from preview runtimes where possible. Stale queued generations could be superseded before consuming a worker.
Priority came from review state, explicit request, and age under a simple policy. It did not favor whoever clicked retry most often.
The queue became observable enough that waiting was understandable. This mattered for adoption: developer experience includes the time before execution and the confidence that another click will not help.
Failure pages kept the previous candidate intact
A failed build produced source-linked diagnostics and retained logs under its generation. A provisioning failure named the external capability it could not establish. A readiness failure exposed protected application logs and the inactive environment URL for diagnosis when safe.
None of those failures replaced the current branch route. The branch page could say “generation 14 failed readiness; generation 13 remains active.”
This was a deliberate tradeoff. The friendly URL did not always represent the latest commit. It represented the latest verified candidate and said when that differed from branch head.
An explicit generation URL let a developer inspect a failed candidate without asking all reviewers to encounter it. A manual promotion override required recorded justification and could not bypass hard isolation or identity checks.
Failure became evidence attached to a proposal, not a reason to make the shared review surface unreliable.
The original cleanup job looked for old containers at night. It missed databases, routes, volumes, and builds that failed before creating a container.
R7K1 assigned expiresAt when accepting an environment request. Every created resource carried environment ownership. The lifecycle record existed even if provisioning stopped halfway.
Active review could extend the deadline under policy. Closing or merging the change shortened it after a grace period. Pinned incident or comparison environments required an owner and explicit new deadline.
Before removal, the UI warned on the branch page. Removal detached the route, stopped runtime, deleted synthetic data and credentials, verified owned resources gone, then retained a compact tombstone.
Expiry became part of the environment's contract rather than a best-effort reaction to resource age.
We measured review consequence, not vanity traffic
R7K1 did not need invented claims about productivity percentages. I looked for concrete behavior changes.
Review discussions began linking to the exact generation and describing interaction states. Responsive and keyboard issues appeared before merge. Two alternatives could remain open together instead of taking turns on one shared server. People outside the repository could verify wording and flow without a local runtime.
Operationally, duplicate artifact builds decreased after manifest reuse. Orphan-resource scans became boring when lifecycle ownership improved. The branch page reduced repeated “is this the latest?” questions because commit identity was visible.
There were negative effects. Easy URLs encouraged more previews, which increased queue and cleanup load. Some reviewers treated a working preview as proof that code quality was acceptable. The diff, tests, and architecture review remained necessary.
The tool improved access to behavior. It did not replace other forms of judgment.
R7K1 initially reported one preview cost estimate. That hid whether expense came from repeated builds, idle runtime, stored artifacts, or retained fixture data.
The environment record separated build compute, running allocation, artifact storage, data storage, and route lifetime. The values were approximate enough for a small system and precise enough to reveal waste.
Manifest reuse reduced build compute. Debouncing removed superseded queue work. Expiry reduced idle runtime. Artifact retention policy addressed storage without deleting active rollback evidence. Each optimization followed the resource it could actually change.
This prevented a blunt response such as shortening every preview to four hours, which would reduce runtime cost while making asynchronous review frustrating. Active work could extend under an owner; abandoned work disappeared predictably.
Cost became another product constraint attached to lifecycle state. A URL was convenient because the platform paid for a running environment, and that payment needed a visible beginning and end.
The project kept sharp limits
R7K1 was not production. Synthetic data could miss real scale and unusual histories. Shared preview hosts had different redundancy and performance. External integrations used sandboxes or stubs. Email, payments, and destructive actions were disabled or redirected.
The preview header named those differences. It did not claim production parity because a container image matched.
Some changes could not be previewed safely under the current model, especially infrastructure work requiring privileged host access. Those received a different controlled environment rather than exceptions added to every branch.
Long-running collaborative state was also a poor fit for aggressive expiry. R7K1 served review candidates, not permanent test accounts.
The system's value came from making a broad, common class of product change observable. Refusing unsuitable workloads kept its security and lifecycle understandable.
Developer experience is production engineering
R7K1 was described as an internal tool, which sometimes invited lower standards. In practice, it sat on the path between an idea and production. When it was unreliable, teams either delayed review or bypassed it. Its queue, security boundary, logs, cleanup, and recovery deserved production discipline because they shaped production decisions.
R7K1 also needed product maintenance. Fixture schemas evolved. Browser expectations changed. Build recipes accumulated. Host capacity and certificate automation required review. The internal label did not reduce those responsibilities; it removed paying customers as a forcing function.
I kept the platform narrow and made its owner visible. New features had to improve the review path or reduce operational risk. A request to add arbitrary persistent services to every preview faced the full lifecycle and isolation cost, not only the ease of starting another container.
This discipline prevented the internal tool from becoming an unowned imitation of production infrastructure.
The lesson I carried forward was not that every team needs a preview platform. It was that review quality depends on the cost of making work observable. If seeing a change requires ceremony, people will review less of the change.
R7K1's technical breakthrough—the normalized build manifest—mattered because it kept that visibility affordable as proposals multiplied. Its product breakthrough was simpler: the system gave unfinished work a stable place, identity, limits, and ending.
The architecture connected earlier lessons. M31V contributed immutable candidates and atomic activation. X8B6 contributed durable intent and receipts. T04P contributed scoped readiness instead of one green light. Later projects would carry those ideas into increasingly complex temporary systems.
Every branch deserved a URL because every meaningful idea deserved to be experienced before it became permanent.