Skip to content

Commit 48f13df

Browse files
BboyAkersclaude
andauthored
docs: add Multiple Applications on One Cluster guide (#586)
* docs: add Multiple Applications on One Cluster guide Add a guide covering how to run multiple applications on a single Harper cluster: how components coexist, how to isolate them via routes, data, and roles, and how to deploy across every node. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add Multiple Applications guide to Learn sidebar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: address PR review on multiple-applications guide - Rename config file references to harper-config.yaml (was harperdb-config.yaml) - Use the harper CLI consistently (was harperdb) - Convert the restart note to a :::note admonition - Fix broken doc links to resolve to /reference/v5/ targets Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: expand multiple-applications routing + isolation (PR review) Address maintainer review requesting fuller routing and isolation coverage: - Make routing the backbone: new "Routing requests to each application" section covering path-based routing (urlPath), the server.http() middleware chain, and host-based routing via request.host - Add "What co-located applications share" section documenting the module loader context isolation model (isolated module caches, shared process-wide data/APIs, shared process lifecycle) - Note SNI certificate configuration for serving multiple domains Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: make middleware example match its prose (PR review) The middleware-chain example described dispatching on a custom header but showed a path-prefix check — a case path-based routing already covers. Switch the example to a custom-header check so it demonstrates what the surrounding text actually recommends the chain for. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document declarative host routing (PR review) Host-based routing led with manual request.host branching, but Harper 5.2 has first-class virtual-host routing: declaring host in a component's config.yaml auto-scopes its whole middleware chain via the scoped server API. Lead with that, show server.http({ host }) as the programmatic equivalent, and reserve manual branching for dispatch that needs runtime logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: mount applications from the root config; correct isolation claims Addresses the outstanding review on #586, updated for #612 (root-config application mounts). - Routing: make the root `harper-config.yaml` application entry the primary mount (`host`/`urlPath`), with `deploy_component` as the deploy-time equivalent and `server.http(handler, { host, urlPath })` as the programmatic one. Branching on `request.host` is now reserved for genuinely custom dispatch. Adds a v5.2.0 badge. - Adds "What a mount does not do": a mount is not a resource namespace (exports land in one instance-wide registry, so duplicate resource names still conflict), and it does not host-constrain `fastifyRoutes` (a host-mounted app declaring them fails to load). Drops `fastifyRoutes` from the primary config example; plugin `urlPath` now positions a plugin within the app, composing with the mount. - Data: renamed to "Namespacing data by database" and states plainly that a database is a namespace, not an enforced boundary — every co-located component can reach any database through the shared `databases` object, and buggy or untrusted code is not prevented from crossing it. Real isolation requires a separate instance or cluster. - Roles: replaced the per-application enforcement claim with the shared model — every `roles.yaml` reconciles into the instance-wide role registry (same name = same role, last load wins), users and sessions are instance-wide. Recommends application-prefixed role names with permissions scoped to each application's database. - Replaces the deprecated `runFirst` option in the middleware example with the `name`/`before`/`after` ordering guidance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: a root-config entry may point to a package, not must A route-only entry (no package) is valid and is how a payload-deployed application gets mounted, so the declarative-registration section no longer states that every entry points to a package. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a563b46 commit 48f13df

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
---
2+
title: Multiple Applications on One Cluster
3+
---
4+
5+
# Multiple Applications on One Cluster
6+
7+
A single Harper cluster can host any number of applications side by side. Because the database, cache, application logic, messaging, and search run within one distributed runtime, an application is not a group of containers wired together—it is a [component](/reference/v5/components/overview), a self-contained module that Harper loads and serves. You add an application to a cluster by registering another component, not by provisioning a new cluster.
8+
9+
This guide covers how to run multiple applications on one cluster: how they coexist, how requests are routed to each one, how far you can separate them, and how to deploy them across every node.
10+
11+
## Applications are components
12+
13+
The unit you deploy in Harper is a component. In the [Operations API](/reference/v5/operations-api/overview) and CLI, "component" refers to an application—a collection of schemas, resources, routes, and static assets that Harper loads and serves.
14+
15+
Two consequences follow:
16+
17+
- **A cluster is a shared resource.** To run a new application, register another component on an existing cluster rather than creating a new cluster.
18+
- **Co-located applications share the runtime.** Because they run within the same converged process, one application's resources can access another application's tables through an in-process call—no cross-service HTTP and no separate connection pool.
19+
20+
Harper isolates each application's module context automatically (see [What co-located applications share](#what-co-located-applications-share)), and it enforces one boundary per application: **routing**—which hostname and URL prefix an application answers on. Beyond that, co-located applications share the instance. Separate databases and distinct role names give you **data namespacing** and **workable access control**, but they are conventions you maintain rather than walls the runtime enforces. The sections below cover all three, and [When to co-locate](#when-to-co-locate) covers the cases where you should reach for a separate cluster instead.
21+
22+
## What co-located applications share
23+
24+
Harper runs as a single process. Every co-located application shares that process and its worker threads, so it is worth being precise about what is isolated between applications and what is not.
25+
26+
- **Module contexts are isolated.** Harper loads each application's JavaScript in its own module context using Node.js's VM module loader, giving every application a distinct module cache. One application's modules, imports, and module-scoped state are not visible to another, so two applications can depend on different packages—or different versions of the same package—without colliding.
27+
- **The data layer and Harper APIs are shared.** The objects you reach through the `harper` package or as globals—`tables`, `databases`, and the rest—are the same live, process-wide objects in every application. A record written by one application is immediately visible to every other, and any application can read or write another's tables in-process. This is what makes co-location efficient, and it is why separate databases are a [namespacing convention](#namespacing-data-by-database) rather than an enforced boundary.
28+
- **Users, roles, and sessions are instance-wide.** Harper's RBAC belongs to the instance, not to an application. Every application's `roles.yaml` reconciles into the same instance-wide role registry, and a user authenticates against the instance as a whole. See [Access control is instance-wide](#access-control-is-instance-wide).
29+
- **The process is shared.** Because every application runs in one process, operational actions apply to all of them: restarting the instance restarts every co-located application, and applications cannot change the process working directory. Plan restarts and deployments with the whole instance in mind.
30+
31+
For the full model, see the [JavaScript Environment](/reference/v5/components/javascript-environment) reference.
32+
33+
## Structuring each application
34+
35+
Every Harper application is configured with a `config.yaml` file in the root of its directory. This file specifies which built-in plugins the application uses—`rest`, the `graphqlSchema` loader, custom JavaScript resources, static file serving, and others:
36+
37+
```yaml
38+
# listings-api/config.yaml
39+
rest: true
40+
graphqlSchema:
41+
files: '*.graphql'
42+
jsResource:
43+
files: 'resources.js'
44+
roles:
45+
files: 'roles.yaml'
46+
static:
47+
files: 'web/**'
48+
urlPath: 'assets'
49+
```
50+
51+
A `config.yaml` **completely replaces** the default configuration; it is not merged with Harper's defaults. Each application therefore defines its own surface area explicitly. Paths declared here are internal to the application—they position a plugin _within_ the application, not on the instance. Where the application itself is served is a separate, deployment-time decision, covered next.
52+
53+
For the full list of options, see the [Component Configuration](/reference/v5/components/overview#configuration) and [Built-In Extensions](/reference/v5/components/overview#built-in-extensions-reference) reference pages.
54+
55+
## Routing requests to each application
56+
57+
Applications on the same instance share a single HTTP listener and port (`9926` by default). Every request enters one layered middleware chain, and routing determines which application handles it. Harper routes by **hostname**, **URL prefix**, or both, with no dispatch code in the application.
58+
59+
### Mounting an application
60+
61+
<VersionBadge version="v5.2.0" />
62+
63+
Where an application is served is a deployment concern, so declare it on that application's entry in the root `harper-config.yaml` (usually `~/hdb/harper-config.yaml`)—not in the application's own `config.yaml`, which a given environment cannot remap:
64+
65+
```yaml
66+
# ~/hdb/harper-config.yaml
67+
listings-api:
68+
package: my-org/listings-api#v1.4.0
69+
host: listings.example.com
70+
urlPath: /v1
71+
admin-dashboard:
72+
package: my-org/admin-dashboard#v0.9.0
73+
host: admin.example.com
74+
```
75+
76+
Every handler the application registers—HTTP, WebSocket, and upgrade—is then served under that mount, and Harper removes the `urlPath` prefix from the pathname before invoking the chain. Application code addresses itself mount-relative and does not need to know where it is mounted. A request that matches no mounted chain falls through to the default middleware chain.
77+
78+
Because routing lives in the root config, the same application package can be mounted at a different hostname or path per environment without editing the application. An entry does not need a `package`—routing applies to any application in the components root, however it was deployed.
79+
80+
The same values can be set at deploy time:
81+
82+
```bash
83+
harper deploy_component \
84+
project=listings-api \
85+
package=my-org/listings-api#v1.4.0 \
86+
host=listings.example.com \
87+
urlPath=/v1
88+
```
89+
90+
Harper selects the most specific matching mount: host and path together, then host alone, then path alone, with longer path prefixes taking precedence. Host matching ignores the port and is case-insensitive; IPv6 hosts are given as a bare literal (`::1`), not bracketed. For the complete rules, see [HTTP middleware routing](/reference/v5/http/overview#middleware-routing) and the [`deploy_component`](/reference/v5/operations-api/operations#deploy_component) parameters.
91+
92+
### Placing routes within an application
93+
94+
A plugin's own `urlPath`, declared in the application's `config.yaml`, positions that plugin _within_ the application. The mount composes with it rather than replacing it, so an application's internal structure survives being relocated: with the `config.yaml` and root config above, `web/**` is served at `listings.example.com/v1/assets/`. A plugin that declares no `urlPath` is served at the mount itself.
95+
96+
When a plugin's `urlPath` is `.` or begins with `./`, Harper prepends the plugin name automatically.
97+
98+
An application can also declare `host` on an individual plugin, but a `host` on the root-config entry overrides it—the operator's choice of hostname wins over the one the application shipped.
99+
100+
### What a mount does not do
101+
102+
A mount is a routing prefix, not an isolation boundary. Two limits matter when several applications share an instance:
103+
104+
- **A mount does not namespace resources.** REST endpoint paths come from the resources and tables an application exports, and those exports land in a single instance-wide registry. Mounting namespaces the external URL an application answers on; it does not make two applications' exports independent. Two applications that both export a `User` resource still conflict, wherever each is mounted. Give each application uniquely named resources or, preferably, its own database.
105+
- **A mount does not host-constrain Fastify routes.** [`fastifyRoutes`](/reference/v5/fastify-routes/overview) registers as a global fallback outside the routed middleware chain, so those routes answer on every hostname. A `urlPath` mount does apply—it becomes the Fastify route prefix—but a `host` mount does not, and Harper refuses to load a host-mounted application that declares `fastifyRoutes` rather than silently serving it unconstrained. Port those routes to [custom resources](/reference/v5/resources/overview) or `server.http()` before mounting the application by host.
106+
107+
### Custom dispatch in the middleware chain
108+
109+
Components add handlers to the chain with the [`server.http()`](/reference/v5/http/api#serverhttplistener-options) API. Each handler either returns a `Response` to handle the request or calls `next(request)` to pass it to the next handler:
110+
111+
```js
112+
server.http((request, next) => {
113+
if (request.headers.get('x-app-target') === 'listings') return handleListings(request);
114+
return next(request);
115+
});
116+
```
117+
118+
Handlers run in registration order; `name`, `before`, and `after` position an entry explicitly relative to another.
119+
120+
`server.http()` accepts `host` and `urlPath` directly, which is the programmatic equivalent of the root-config mount:
121+
122+
```js
123+
server.http(handleAdmin, { host: 'admin.example.com', urlPath: '/api' });
124+
```
125+
126+
Reach for the middleware chain when a mount is not enough—to dispatch on a custom header, as above, to rewrite a path before it reaches an application, or to resolve a target host at runtime by branching on `request.host` inside a handler. For fixed hostnames and prefixes, prefer the declarative mount: Harper matches it before any handler code runs, and an operator can change it without a code change. See the [HTTP API](/reference/v5/http/api) reference for the full `Request` object and the [`HttpOptions`](/reference/v5/http/api#httpoptions) matching and ordering rules.
127+
128+
### Serving multiple domains over HTTPS
129+
130+
When serving several hostnames over TLS, configure a certificate per domain with [SNI](/reference/v5/http/tls#multi-domain-certificates-sni): define `tls` as an array with a `host` entry for each domain. SNI selects the certificate; the mount's `host` selects the application's middleware chain—the two are independent and both are needed to serve an application on its own HTTPS domain.
131+
132+
## Namespacing data by database
133+
134+
Give each application its own database. Its tables then belong to it by name, its resources address them without qualification, and the applications do not collide in the table namespace.
135+
136+
This is namespacing, not enforced isolation. `databases` is a process-wide object, so every co-located component can reach every database through it—a database boundary is a convention that well-behaved application code respects, and nothing in the runtime prevents buggy or untrusted component code from crossing it. Treat co-located applications as sharing one trust domain, and vet component code the way you would vet code you are adding to the same service.
137+
138+
That shared access is also what makes co-location useful: when one application needs data from another—an `admin-dashboard` reading from the `listings-api`—it queries the table directly, in-process, rather than opening a network connection to another service.
139+
140+
When a boundary has to hold against code you do not fully trust, or against a security or compliance requirement, put the application on a separate instance or cluster. That is the only boundary Harper enforces for data.
141+
142+
## Access control is instance-wide
143+
144+
Each application can include its own [`roles.yaml`](/reference/v5/users-and-roles/configuration) file, which is convenient—but the roles it declares are not scoped to that application. Harper reconciles every declared role into the instance's single role registry: a role that does not exist is created, and a role that already exists has its permissions overwritten to match the declaration. Users and sessions authenticate against the instance as a whole, and a user's role applies wherever they are authenticated, not just within the application that defined it.
145+
146+
Two practices keep this workable when several applications share an instance:
147+
148+
- **Prefix role names per application.** `listings-reader` and `admin-dashboard-reader` are two roles; two applications that both declare `reader` are one role, and whichever loads last wins—silently changing the permissions the other application expects.
149+
- **Scope each role's permissions to that application's database.** A user-defined role grants nothing unless it is granted explicitly, so a role that names only its own database cannot read another application's tables even though the RBAC system is shared. Grant a role access to a second application's database only when that access is intended.
150+
151+
Because the permission model is shared, "different access rules per application" means different roles within one RBAC system, not separate systems. If two applications must not share a user directory or a role namespace at all, run them on separate instances.
152+
153+
## Registering multiple applications
154+
155+
There are two ways to add applications to an instance.
156+
157+
### Declaratively, through the instance config
158+
159+
Harper reads applications from the `harper-config.yaml` file in its root path (usually `~/hdb`). An entry may point to a package—a GitHub repository, an npm package, a tarball, a local path, or a URL—and may carry that application's `host` and `urlPath`, as shown in [Mounting an application](#mounting-an-application). Either half stands on its own: an entry with only a `package` installs an application served on the default chain, and an entry with only routing keys mounts a component that was deployed some other way, such as by `harper deploy`:
160+
161+
```yaml
162+
# ~/hdb/harper-config.yaml
163+
listings-api:
164+
package: my-org/listings-api#v1.4.0
165+
inventory-service:
166+
package: my-org/inventory-service#v2.1.0
167+
admin-dashboard:
168+
package: my-org/admin-dashboard#v0.9.0
169+
```
170+
171+
Reference a Git repository with a semver tag to lock each application to a specific, reproducible version. Harper translates these entries into a `package.json` file, runs an install to resolve them, and loads each as a component. The entry name is arbitrary and does not need to match a package dependency name.
172+
173+
### Imperatively, through the CLI
174+
175+
To deploy from an application directory, run `harper deploy` inside the project. This packages the current directory and sends it to the active instance:
176+
177+
```bash
178+
cd listings-api
179+
harper deploy
180+
```
181+
182+
A payload deploy like this cannot carry `host` or `urlPath`; mount a payload-deployed application by adding those keys to its entry in the root `harper-config.yaml`.
183+
184+
For local iteration, `harper dev .` runs the application and watches for file changes, restarting worker threads on edit. See the [Applications reference](/reference/v5/components/applications) for the complete set of deployment options.
185+
186+
## Deploying across the cluster
187+
188+
The preceding sections deploy an application to a single instance. To deploy it across every node in the cluster, include the `replicated=true` parameter.
189+
190+
When deploying in a clustered environment, set `replicated=true` to spread the deployment to all nodes:
191+
192+
```bash
193+
harper deploy_component \
194+
project=listings-api \
195+
package=https://github.com/my-org/listings-api#v1.4.0 \
196+
target=https://cluster-node-1.example.com:9925 \
197+
replicated=true
198+
```
199+
200+
`target` points to a node's operations endpoint. `replicated=true` sends the deployment to the rest of the cluster, extending the "deploy everywhere" behavior of Harper's [replication](/reference/v5/replication/overview) to the application itself.
201+
202+
:::note
203+
Restart afterward to apply the changes—for example, `harper restart target=https://cluster-node-1.example.com:9925 replicated=true`.
204+
:::
205+
206+
On Harper Fabric, a single `harper deploy` from the project directory deploys the application across regions, with replication, routing, and failover managed by the platform.
207+
208+
## When to co-locate
209+
210+
Co-locate applications on one cluster when they share a data domain, benefit from in-process access to each other's tables, or are small enough that separate clusters would sit mostly idle. This covers common cases such as an API, its admin interface, a background worker, and a public site. Co-located applications share a trust domain, so this works best when the same team owns the code, or when you would be willing to run it all in one service.
211+
212+
Use separate clusters when applications require independent scaling, have significantly different availability needs, or need a boundary the runtime actually enforces—tenancy, compliance, an untrusted or third-party component, or a user directory that must not be shared. Choose based on the workload rather than defaulting to one cluster per service.

sidebarsLearn.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ const sidebarsLearn: SidebarsConfig = {
4141
id: 'developers/harper-applications-in-depth',
4242
label: 'Harper Applications in Depth',
4343
},
44+
{
45+
type: 'doc',
46+
id: 'developers/multiple-applications',
47+
label: 'Multiple Applications on One Cluster',
48+
},
4449
{
4550
type: 'doc',
4651
id: 'developers/caching-with-harper',

0 commit comments

Comments
 (0)