From the Portable application delivery series
What Actually Happens When You Deploy a Full-Stack Monorepo?
Follow a repository from safe discovery through dependency boundaries, managed builds, resource bindings, full-stack preview and an identified production release.
By BlinkHost Engineering · Published 01/09/2026
What you will understand
- — Distinguish application, frontend and dependency roots in a monorepo.
- — Understand why backend discovery cannot safely imply backend compatibility.
- — Trace source through preview, verified artifacts and release promotion.
A repository can be perfectly understandable to its authors and completely ambiguous to a deployment system.
Consider this ordinary layout:
acme-platform/
├── apps/
│ ├── web/
│ │ ├── package.json
│ │ └── src/
│ └── admin/
│ ├── package.json
│ └── src/
├── services/
│ └── billing/
│ ├── pyproject.toml
│ └── app.py
├── packages/
│ └── design-system/
├── pnpm-workspace.yaml
└── pnpm-lock.yaml
A person sees two frontends, a service and a shared package. A safe importer sees several questions. Which application is being deployed? Where should dependencies be installed? Which command produces browser assets? Is services/billing a long-running server, a function candidate or unrelated source? Which files are allowed into the public artifact?
Deployment begins by answering those questions without executing repository code.
Stage 1: acquire source without trusting it
The first boundary should treat every path, archive member and manifest value as untrusted input.
Safe acquisition rejects path traversal, symbolic-link surprises, duplicate paths, oversized repositories and reserved filenames before extraction. It records the repository identity and commit, then creates an immutable source snapshot for the build. A later branch update must not change the meaning of an in-progress release.
This is also where secrets need an early warning. Importing .env.production, a private key or a cloud credential is not made safe by placing it in a private repository. Repository access and runtime-secret access are different permissions with different lifecycles.
Stage 2: propose applications conservatively
Discovery should produce proposals, not silently make architectural decisions.
Useful signals include:
package.jsonand its scripts;- committed lockfiles;
- workspace files;
- framework dependencies;
index.htmlfor a static application;- conventional application directories such as
weborfrontend; - an explicit platform manifest.
The important word is conservatively. Finding React does not reveal the intended output directory. Finding pyproject.toml does not prove that a Python web server can run in a bounded function environment. If more than one application is plausible, the developer should choose.
BlinkHost currently recognizes static HTML and supported Astro, React, Solid, Svelte and Vue frontend targets. It also understands npm, pnpm, Yarn and Bun declarations. Unsupported server-rendered modes should fail with a useful explanation instead of being flattened into static output and appearing to work.
Stage 3: distinguish three different roots
Many failed monorepo builds come from treating “the root” as a single idea. In practice there are at least three:
- Application root — the boundary of the selected application inside the repository.
- Frontend root — the directory containing the frontend entry and build configuration.
- Dependency root — the directory where the package manager must see its workspace and lockfile.
For apps/web, the frontend root may be apps/web while the dependency root remains . because the root lockfile and shared workspace packages are required. Running installation inside apps/web would ignore that contract. Publishing the repository root would expose far too much.
An explicit manifest removes the ambiguity:
schema: blinkhost/v1
application:
root: .
frontend:
root: apps/web
dependency_root: .
framework: react
package_manager: pnpm
install: pnpm install --frozen-lockfile
build: pnpm --filter web build
dev: pnpm --filter web dev
output: apps/web/dist
modules:
- name: customer-api
path: _server_islands/customer-api
language: typescript
entrypoint: index.ts
abi: blinkhost-wasi-1
sdk: "1.1"
preview:
enabled: true
database_mode: none
The manifest declares intent. It does not grant capabilities, contain secrets or override plan limits. Those remain server-side decisions.
Stage 4: establish a deterministic dependency graph
Workspaces make code reuse pleasant, but they broaden the build input. A cache key for apps/web cannot depend only on apps/web/package.json if the application imports packages/design-system.
A sound dependency fingerprint includes the selected roots, package-manager version, lockfile, relevant workspace manifests and build configuration. A changed shared package should invalidate the right cache. A different project must never inherit another tenant's mutable dependency directory.
Locked installation is important for a second reason: diagnosis. When a clean build fails, a developer should be able to reproduce the dependency graph rather than investigate whatever versions happened to resolve that morning.
Stage 5: build browser and server concerns separately
A full-stack repository is not necessarily one executable.
The frontend build may run in a browser development environment or a managed build environment and produce static assets. Backend source requires a runtime contract. There are several legitimate outcomes:
- a supported bounded function is compiled through a managed toolchain;
- a persistent server is deployed to an appropriate container or VM product;
- the frontend calls an external API that remains outside this deployment;
- the backend directory is unrelated to the selected application and is excluded.
Creating a folder with a special name cannot safely turn arbitrary server code into a deployable backend. The platform still has to know the language, entrypoint, ABI, capabilities and resource limits. It must build the source itself or otherwise verify the executable artifact.
BlinkHost uses _server_islands/<name>/ for managed module source. Rust and TinyGo functions are generally available; Python, JavaScript and TypeScript functions carry explicit beta contracts. Long-running Django, Flask, FastAPI, Express or similar servers are not silently converted into these functions. That boundary prevents a convenient import experience from becoming a misleading portability promise.
Stage 6: connect resources by name, not hidden wiring
Databases, object storage, secrets and background work should be explicit bindings. Source can request a binding such as CUSTOMER_DB; the control plane decides which project-scoped resource fulfils it in preview and production.
This separation makes four things possible:
- preview can use an isolated branch or disposable resource;
- production credentials never enter the browser build;
- plan and policy limits remain enforceable;
- exporting source does not export provider credentials.
It also makes missing configuration diagnosable. “Binding CUSTOMER_DB is not provisioned” is more useful than a connection failure several layers later.
Stage 7: preview the selected system, not an imitation
A useful preview proves the frontend against eligible backend modules and resources while preserving environment boundaries.
The browser should not receive production database credentials. Preview capabilities should be short-lived, project-scoped and restricted to known routes. Requests need a clear bridge so a frontend calling /_server_islands/customer-api/orders reaches the selected preview module without teaching application code a temporary infrastructure hostname.
The preview is evidence, not the production release. It may use different data, shorter retention and tighter limits.
Stage 8: promote identified artifacts
At the end of the build there may be several outputs: frontend assets, verified function modules, database migration declarations and configuration references. A release manifest ties them to the source snapshot, build records and policy.
Promotion should reuse those artifacts. Rebuilding between preview and production weakens the claim that the reviewed output is what shipped. Rollback should select a prior compatible release rather than reconstruct it from an old branch.
What the developer should see
Most of this machinery should not become interface noise. A developer needs five clear decisions:
- We found these application candidates.
- This is the selected frontend and dependency root.
- These backend directories match a supported runtime; these do not.
- These resources are required, missing or ready.
- This exact source revision produced this preview and release.
Good deployment systems do not pretend every repository is simple. They make the complexity legible, preserve the developer's architecture where it is compatible, and stop before inventing intent.
Further reading
Repository readiness scanner
Paste relative file paths—never file contents. The scan runs locally and makes no network request.
apps/web, repository root
blinkhost.yaml can disambiguate application, frontend and dependency roots.
Keep it committed and aligned with the declared package manager.
Source is a candidate for a managed build; repository-supplied compiled output is not trusted.
Example monorepo tree
Use this names-only example to explore the scanner without exposing a repository.
Download repository-readiness-example.txtDisclosure: The BlinkHost portions describe the public repository and runtime contract as verified for this revision; framework support remains lifecycle- and version-specific.