How Modules and Components Are Organized in Openship: Monorepo Architecture Deep Dive

Openship organizes its modules as a TypeScript monorepo with self-contained packages under packages/, using a factory pattern in @repo/adapters to wire runtime, routing, SSL, and system adapters based on the deployment target.

The oblien/openship repository implements a modular architecture designed to support cloud, self-hosted, and desktop deployments from a single codebase. Understanding how modules or components are organized in openship reveals a clean separation of concerns through domain-driven packages and a dynamic adapter system. The architecture leverages a Platform factory pattern that selects concrete implementations at initialization time, allowing the core business logic to remain agnostic of infrastructure details.

Monorepo Layout and Package Structure

The repository follows a monorepo pattern with two primary directories: packages/ for shared libraries and apps/ for deployable applications. Each package represents a logical layer of the platform and exposes its public API via a package.json file, enabling tree-shaking at build time so only required code reaches the final bundle.

The top-level organization includes:

  • packages/adapters – The abstraction layer containing runtime, infrastructure, and system adapters.
  • packages/core – Generic utilities and helper functions used across the system.
  • packages/ui – React component library for the web dashboard.
  • packages/onboarding – CLI onboarding flow and validation logic.
  • packages/db – Drizzle ORM schema definitions for all persisted entities.
  • packages/db-email – SMTP email client wrapper.
  • apps/web – The Next.js web dashboard application (configured via apps/web/tsconfig.json).

The Three Core Architectural Layers

The codebase revolves around three fundamental concepts that define how components interact.

1. Adapters (@repo/adapters)

The adapters package provides the abstraction layer that glues the API to underlying runtime environments. Located in packages/adapters/, this is the most critical module for understanding how openship handles different deployment targets. The relationship between layers is documented in packages/adapters/docs/ARCHITECTURE.md, which illustrates how targets, layers, and wiring interact.

Key files include:

2. Core (@repo/core)

The core package contains shared utilities that have no external dependencies on infrastructure. These helpers support the rest of the system with functions like TOML parsing and slug formatting.

Important files:

3. Domain-Specific Packages

These modules handle specific business concerns:

The Platform Factory Pattern

The heart of openship's module organization is the Platform factory pattern implemented in packages/adapters/src/platform.ts. This factory creates a singleton Platform object that wires together concrete implementations based on the deployment configuration.

Initialization Flow

At server startup, the CLI or API calls initPlatform(config), which reads the deployment target (cloud, selfhosted, or desktop) and constructs a Platform object with four key properties:

{
  runtime: RuntimeAdapter,    // Docker or Bare metal
  routing: RoutingProvider,   // Nginx, Cloud, or Noop
  ssl: SslProvider,          // Certificate management
  system: SystemManager | null,  // Host setup and validation
  executor: CommandExecutor | null // Local or SSH command execution
}

The Platform is cached via getPlatform() and injected throughout the service layer, ensuring consistent access to infrastructure capabilities.

Adapter Selection Logic

The factory selects implementations based on the target and runtime configuration values:

Cross-Module Communication

Modules communicate through dependency injection and shared interfaces rather than direct imports of concrete classes.

The CommandExecutor abstraction demonstrates this pattern. Depending on configuration, the factory injects either a LocalExecutor (using child_process) or an SshExecutor (via ssh2) into both runtime and infrastructure adapters. This ensures a single connection to the target host and consistent error handling across all operations.

import { initPlatform } from '@repo/adapters';

// Configuration for self-hosted Docker deployment
const cfg = {
  target: 'selfhosted',
  runtime: 'docker',
  docker: { socketPath: '/var/run/docker.sock' },
  nginx: { certEmail: 'admin@example.com' },
  ssh: undefined,  // local execution
};

async function start() {
  // Initialize the singleton Platform
  const platform = await initPlatform(cfg);
  
  // Access wired components
  const { runtime, routing, ssl, system } = platform;
  
  // Execute platform operations
  await runtime.build({ name: 'my-app' }, console.log);
  await routing.registerRoute({ 
    domain: 'app.example.com', 
    targetUrl: 'http://localhost:3000' 
  });
  await ssl.provisionCert('app.example.com');
  
  if (system) await system.requireFeature('deploy');
}

This code resolves to concrete implementations chosen by the factory, demonstrating how the same business logic operates across Docker, bare-metal, cloud, and desktop targets without modification.

Summary

  • Openship uses a monorepo structure with self-contained packages under packages/ and applications under apps/.
  • The @repo/adapters package implements a factory pattern that wires runtime, routing, SSL, and system components based on deployment targets.
  • Platform initialization occurs via initPlatform() in packages/adapters/src/platform.ts, which creates a singleton with concrete adapter implementations.
  • Runtime adapters include Docker and bare-metal variants, while infrastructure adapters handle Nginx, cloud, or no-op routing.
  • Domain packages (ui, db, onboarding, core) provide specific business functionality without infrastructure dependencies.
  • CommandExecutor abstraction ensures consistent local or remote execution across all modules.

Frequently Asked Questions

What is the purpose of the Platform factory in openship?

The Platform factory in packages/adapters/src/platform.ts serves as the central wiring mechanism that selects and configures concrete implementations of runtime, routing, SSL, and system adapters based on the deployment target. It creates a singleton Platform object cached via getPlatform(), allowing the same business logic to run on cloud servers, self-hosted Docker hosts, bare-metal SSH machines, or desktop environments without code changes.

How does openship handle different runtime environments?

Openship handles different runtime environments through the adapter pattern implemented in the @repo/adapters package. The factory instantiates either DockerRuntime (from packages/adapters/src/runtime/docker.ts) for containerized deployments or BareRuntime (from packages/adapters/src/runtime/bare.ts) for direct shell execution. Both implement the same RuntimeAdapter interface, ensuring consistent build and deployment operations regardless of the underlying infrastructure.

What distinguishes the Core package from domain-specific packages?

The packages/core module contains generic, infrastructure-agnostic utilities like TOML helpers (packages/core/src/toml-helpers.ts) and slug formatters (packages/core/src/utils-format-slug.ts) used across the entire system. Domain-specific packages such as packages/ui, packages/db, and packages/onboarding handle specific business concerns with potential external dependencies. Core utilities never interact with Docker, SSH, or database connections directly, maintaining a clean boundary between pure logic and infrastructure code.

Can I add custom adapters to the openship platform?

Yes, you can extend openship by creating new adapter implementations that conform to the existing interfaces defined in packages/adapters. To add a custom runtime, implement the RuntimeAdapter interface and register it in the factory at packages/adapters/src/platform.ts. Similarly, custom routing providers can extend the infrastructure abstraction by following the patterns in packages/adapters/src/infra/nginx.ts or packages/adapters/src/infra/cloud.ts, then wiring them into the Platform initialization logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →