Subdomain Model
Grove projects can contain multiple apps, each served on its own subdomain. In development,
<app>.localhost:<port>routes to the correspondingapps/<app>/directory. In production, routing uses theX-Tenant-IDheader set by your reverse proxy.
Prerequisites: File-Based Routing, Server Configuration. What you'll learn: How multi-app projects work, the tenant ID format, dev-mode subdomain routing, and single-app defaults.
App Discovery
Grove scans the apps/ directory at startup. Each subdirectory becomes a separate app with its own pages, layouts, and API routes:
apps/
main/ # app "main"
layout.html
index.html
docs/ # app "docs"
layout.html
index.html
admin/ # app "admin"
layout.html
index.html
Each app is fully independent -- it has its own routing tree, layouts, static assets, and data loaders.
Tenant ID Format
Each app gets a tenant ID composed of the project ID and app name:
{project_id}-{app_name}
For a project with project_id = "grove-docs", the tenant IDs would be:
| App directory | Tenant ID |
|---|---|
apps/main/ | grove-docs-main |
apps/docs/ | grove-docs-docs |
apps/admin/ | grove-docs-admin |
Dev Mode Routing
In development, Grove maps subdomains of localhost to apps:
| URL | Routes to |
|---|---|
main.localhost:3000 | apps/main/ |
docs.localhost:3000 | apps/docs/ |
admin.localhost:3000 | apps/admin/ |
localhost:3000 | Embedded dev console |
Bare localhost:<port> (without a subdomain) serves the built-in developer console, which lists available apps and provides project-level tooling.
Production Routing
In production, a reverse proxy (e.g., Nginx, Caddy, or a CDN) sets the X-Tenant-ID header based on the incoming hostname. Grove's tenant_dispatch() reads this header to route the request to the correct app.
Example Nginx configuration:
location / {
proxy_set_header X-Tenant-ID grove-docs-main;
proxy_pass http://localhost:3000;
}
Manzano-hosted edge deployments set X-Tenant-ID automatically, alongside the richer identity-plus-routing header set used for authentication and authorization. See Identity Flow for the complete list of headers the edge stamps on every request (including X-Site-Id, X-Project-Id, X-Distribution-Id, and X-Sub), and the HMAC signature that lets your backend trust them.
Single-App Default
When a project contains only one app, that app becomes the default. It serves all requests without requiring a subdomain or X-Tenant-ID header:
apps/
main/ # only app -- serves on all requests
layout.html
index.html
In this case, both localhost:3000 and main.localhost:3000 serve the same app. The dev console is not shown.
Multi-App Project Structure
A typical multi-app project shares Grove modules across apps while giving each app its own web layer:
grove.toml
orders/
module.grove # shared module
apps/
storefront/
layout.html
index.html
api/orders/route.grove
admin/
layout.html
index.html
api/orders/route.grove
Both apps can reference the same orders module in their API routes, but each has independent page templates and layouts.
See Also
- File-Based Routing -- How files inside an app map to URLs
- Server Configuration -- Server startup and port settings
- API Routes in Apps -- API endpoints within each app
- Identity Flow -- Edge-stamped identity and routing headers