Context
As deployments scale from single-server setups to multi-tenant environments, managing domains, port conflicts, and infrastructure boundaries becomes critical. Currently, OpenShip mixes its internal host containers with user projects, restricts wildcard domains to a single apex, defaults to common developer ports (colliding with local web frameworks), and lacks a dedicated settings tab for the dashboard domain and telemetry.
This proposal establishes a clean boundary between OpenShip's control plane and tenant workloads while expanding domain routing flexibility.
What's being addressed
1. Multi-Wildcard Domains & Collision-Proof Subdomains
- The Problem: OpenShip currently only allows a single wildcard domain per instance. Teams managing multiple brands, staging tiers, or client environments (e.g.
*.staging.acme.com and *.preview.clients.io) cannot register multiple wildcard apexes. In addition, preview subdomains generated purely from raw project names collide when projects share names or are recreated.
- Proposed Solution:
- Add a dedicated
wildcard_domain schema in @repo/db allowing operators to register, label, and designate a default wildcard apex.
- Generate collision-proof subdomains (
${slug}-${randomHex(6)}.${wildcardApex}) upon project provisioning.
2. Control Plane Workspace Isolation & Self-Destruction Guards
- The Problem:
- OpenShip's internal containers (API, Dashboard, Edge) currently surface in the main "Apps & Projects" workspace grid.
- Operators or team members see "Stop", "Restart", or "Delete" buttons on internal control plane cards. An accidental click terminates the API container, instantly locking everyone out of the web interface and requiring manual SSH intervention on the host VM.
- Proposed Solution:
- Exclude
appTemplateId === 'openship' by default from GET /api/projects and GET /api/projects/home.
- Add explicit 403
PROJECT_IS_CONTROL_PLANE rejection guards on POST /api/services/:id/stop, POST /api/services/:id/restart, and DELETE /api/services/:id for internal services.
3. Standardizing Default Internal Ports to 7000-Series (7000-7400)
- The Problem: OpenShip defaults to ports commonly used by local development servers:
3001: Default fallback for React / Next.js / Vite when 3000 is occupied.
4000: Standard port for Express.js / GraphQL APIs.
5000: Standard for Flask / ASP.NET / local Docker registries.
Running local developer workloads or containers frequently results in silent port bind collisions.
- Proposed Solution: Standardize internal control-plane defaults to a dedicated 7000 block with 100-stride spacing (
Dashboard: 7000, API: 7100, Web: 7200, SaaS Dashboard: 7300, SaaS API: 7400), leaving common web ports free for user applications.
4. Instance-Level Dashboard Domain & Telemetry in Settings
- The Problem: Operators previously had to treat OpenShip as a project just to bind a custom domain (e.g.
dev.domain.com) to the dashboard or view host resource usage.
- Proposed Solution:
- Add a dedicated Settings → Domains & Wildcard tab to manage the Dashboard Root Domain with automated Cloudflare DNS verification.
- Add
GET /api/system/telemetry and mount a live Control Plane Health & Telemetry card in Settings → Instance displaying CPU load average, Node RSS/Heap memory, and system uptime.
5. Hardened Loopback Route Ingress Protection
- The Problem: Custom ingress rules could attempt to proxy traffic to host loopback addresses (
127.0.0.1:7100 / 127.0.0.1:7000), potentially exposing internal control plane APIs.
- Proposed Solution: Refactor
isReservedLoopbackPort and isLoopbackHost in public-endpoints.ts to normalize loopback targets (localhost, 127.0.0.1, ::1, [::1]) and reject any tenant routing rules targeting internal management ports.
Proposed Implementation Breakdown
packages/
├── core/
│ └── src/constants.ts # Standardize DEFAULT_PORT (7000, 7100, 7200)
├── db/
│ ├── src/schema/wildcard-domain.ts # Wildcard domain table schema
│ ├── src/repos/wildcard-domain.repo.ts # Wildcard repository queries
│ ├── drizzle/0121_wildcard_and_dashboard_domain.sql # Drizzle migration
│ └── src/dump.ts # Backup catalogue registration
apps/
├── api/
│ ├── src/modules/domains/wildcard-domain.service.ts # Normalization & collision-proof slugs
│ ├── src/modules/system/setup.controller.ts # Dashboard domain endpoints
│ ├── src/modules/system/telemetry.controller.ts # Live host/process telemetry endpoint
│ ├── src/modules/services/service.controller.ts # 403 PROJECT_IS_CONTROL_PLANE guards
│ ├── src/modules/projects/project.controller.ts # Exclude control plane from project grid
│ └── src/lib/public-endpoints.ts # Hardened loopback proxy guards
└── dashboard/
├── src/pages/settings/domains/DomainsSettingsTab.tsx # Dashboard & wildcard domain UI
└── src/pages/settings/instance/InstanceInfo.tsx # Live telemetry & health card
Verification Plan
- Multi-Wildcard Domain Routing:
- Register multiple wildcards (
*.staging.acme.com, *.preview.clients.io).
- Provision projects and verify collision-proof subdomains route through OpenResty ingress correctly.
- Control Plane Isolation:
- Verify OpenShip containers no longer appear in the general projects grid.
- Attempt to call
POST /api/services/:id/stop against an internal service and verify 403 Forbidden.
- Port Standardization:
- Verify dev servers start cleanly on ports 7000/7100 without colliding with local ports 3000, 3001, or 4000.
- Dashboard Domain & Telemetry:
- Configure a custom dashboard domain in Settings and verify DNS auto-provisioning.
- Verify real-time CPU and memory metrics render accurately on the Instance Info tab.
Context
As deployments scale from single-server setups to multi-tenant environments, managing domains, port conflicts, and infrastructure boundaries becomes critical. Currently, OpenShip mixes its internal host containers with user projects, restricts wildcard domains to a single apex, defaults to common developer ports (colliding with local web frameworks), and lacks a dedicated settings tab for the dashboard domain and telemetry.
This proposal establishes a clean boundary between OpenShip's control plane and tenant workloads while expanding domain routing flexibility.
What's being addressed
1. Multi-Wildcard Domains & Collision-Proof Subdomains
*.staging.acme.comand*.preview.clients.io) cannot register multiple wildcard apexes. In addition, preview subdomains generated purely from raw project names collide when projects share names or are recreated.wildcard_domainschema in@repo/dballowing operators to register, label, and designate a default wildcard apex.${slug}-${randomHex(6)}.${wildcardApex}) upon project provisioning.2. Control Plane Workspace Isolation & Self-Destruction Guards
appTemplateId === 'openship'by default fromGET /api/projectsandGET /api/projects/home.PROJECT_IS_CONTROL_PLANErejection guards onPOST /api/services/:id/stop,POST /api/services/:id/restart, andDELETE /api/services/:idfor internal services.3. Standardizing Default Internal Ports to 7000-Series (
7000-7400)3001: Default fallback for React / Next.js / Vite when 3000 is occupied.4000: Standard port for Express.js / GraphQL APIs.5000: Standard for Flask / ASP.NET / local Docker registries.Running local developer workloads or containers frequently results in silent port bind collisions.
Dashboard: 7000,API: 7100,Web: 7200,SaaS Dashboard: 7300,SaaS API: 7400), leaving common web ports free for user applications.4. Instance-Level Dashboard Domain & Telemetry in Settings
dev.domain.com) to the dashboard or view host resource usage.GET /api/system/telemetryand mount a live Control Plane Health & Telemetry card in Settings → Instance displaying CPU load average, Node RSS/Heap memory, and system uptime.5. Hardened Loopback Route Ingress Protection
127.0.0.1:7100/127.0.0.1:7000), potentially exposing internal control plane APIs.isReservedLoopbackPortandisLoopbackHostinpublic-endpoints.tsto normalize loopback targets (localhost,127.0.0.1,::1,[::1]) and reject any tenant routing rules targeting internal management ports.Proposed Implementation Breakdown
Verification Plan
*.staging.acme.com,*.preview.clients.io).POST /api/services/:id/stopagainst an internal service and verify403 Forbidden.