# Best Practices for Developing with the Openship Packages: A Complete Technical Guide

> Master openship package development with this guide. Learn three-layer architecture, platform factory composition, and PNPM workspace best practices for efficient, type-safe applications.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: best-practices
- Published: 2026-07-23

---

**Develop with openship by respecting its three-layer architecture (runtime, infra, system), using the platform factory to compose deployment-specific adapters, and following PNPM workspace conventions with strict TypeScript and Drizzle ORM patterns.**

Openship is a monorepo deployment platform maintained by oblien that bundles a CLI, web dashboard, and desktop application. Following best practices for developing with the openship packages ensures your code remains compatible across cloud, self-hosted, and desktop deployment targets while leveraging the platform's factory-based composition model.

## Understanding the Three-Layer Architecture

Openship organizes functionality into three distinct layers that are composed by a **platform factory**. Understanding these boundaries is essential for writing maintainable code.

### Runtime Layer

The **runtime** layer handles container and process lifecycle operations. Located in `packages/adapters/src/runtime/`, it defines the `RuntimeAdapter` interface implemented by `DockerRuntime`, `BareRuntime`, and `CloudRuntime`. These classes manage build, start, stop, and destroy operations according to the deployment target.

When extending openship, implement the `RuntimeAdapter` interface in [`packages/adapters/src/runtime/types.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/runtime/types.ts) rather than modifying concrete implementations directly. This preserves compatibility with the factory pattern used in [`packages/adapters/src/platform.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/platform.ts).

### Infra Layer

The **infra** layer manages reverse-proxy routing and TLS certificate provisioning. Key types include `RoutingProvider` and `SslProvider`, with concrete implementations like `NginxProvider` in [`packages/adapters/src/infra/nginx.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/infra/nginx.ts) and `CloudInfraProvider` for managed environments.

Keep infra concerns separate from runtime logic. The `NginxProvider` handles certificatebot integration and route registration without direct knowledge of container internals, ensuring clean separation between how services run and how they are exposed.

### System Layer

The **system** layer performs prerequisite checks and tool installation. The `SystemManager` class in [`packages/adapters/src/system/setup.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/setup.ts) coordinates `CommandExecutor` to verify Docker, Traefik, Git, and other dependencies before deployment begins.

Use `SystemManager` for any prerequisite validation rather than implementing custom checks. This ensures consistent behavior across CLI and desktop environments.

### Platform Factory Composition

These layers assemble via the factory function in [`packages/adapters/src/platform.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/platform.ts):

```typescript
export async function createPlatform(config: PlatformConfig): Promise<Platform> {
  switch (config.target) {
    case "cloud":       return createCloudPlatform(config);
    case "desktop":     return createDesktopPlatform(config);
    case "selfhosted":  return createSelfHostedPlatform(config);
  }
}

```

The factory resolves the appropriate runtime/infra/system combination at startup based on `CLOUD_MODE` and `DEPLOY_MODE` environment variables. This enables tree-shaking of unused code while supporting three distinct deployment targets from a single codebase.

## Navigating the PNPM Workspace Structure

Openship uses **PNPM workspaces** defined in [`pnpm-workspace.yaml`](https://github.com/oblien/openship/blob/main/pnpm-workspace.yaml):

```yaml
packages:
  - "apps/*"
  - "packages/*"

```

The repository divides into:

- **`apps/`** – Entry points including `cli`, `dashboard`, `desktop`, `api`, `email`, and `web`
- **`packages/`** – Reusable libraries: `adapters`, `core`, `db`, `db-email`, `onboarding`, `ui`

All packages extend the root [`tsconfig.base.json`](https://github.com/oblien/openship/blob/main/tsconfig.base.json), enforcing consistent compiler options and strict TypeScript checking across the entire repository. When adding new packages, ensure they inherit from this base configuration to maintain type compatibility.

## Implementing Database Changes with Drizzle ORM

The database layer uses **Drizzle ORM** with schemas defined in `packages/db/src/schema/`. Each table resides in its own TypeScript file, such as [`packages/db/src/schema/service.ts`](https://github.com/oblien/openship/blob/main/packages/db/src/schema/service.ts):

```typescript
export const service = pgTable("service", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  // …
});

```

Follow this three-step pattern when adding new models:

1. Create a schema file under `packages/db/src/schema/`
2. Add a corresponding repository under `packages/db/src/repos/` (e.g., [`service.repo.ts`](https://github.com/oblien/openship/blob/main/service.repo.ts))
3. Write unit tests alongside the repository (e.g., [`service.repo.test.ts`](https://github.com/oblien/openship/blob/main/service.repo.test.ts))

Repository files provide typed query helpers used throughout the platform. For example:

```typescript
// packages/db/src/repos/new-entity.repo.ts
import { db } from "../client";
import { newEntity } from "../schema/new-entity";

export const newEntityRepo = {
  async create(data: { name: string }) {
    return await db.insert(newEntity).values(data).returning();
  },

  async findById(id: number) {
    return await db.select().from(newEntity).where(eq(newEntity.id, id));
  },
};

```

## Building CLI and API Features

The CLI in `apps/cli` serves as a thin wrapper around the platform API. Commands like `openship init` and `openship deploy` invoke the same core logic used by the web dashboard (`apps/api`), ensuring behavioral parity between interfaces.

### Reusing Validation Logic

The `onboarding` package centralizes user-facing validation. Functions like `validateServerAddress` in [`packages/onboarding/src/validation.ts`](https://github.com/oblien/openship/blob/main/packages/onboarding/src/validation.ts) enforce consistent input handling:

```typescript
export function validateServerAddress(ip: string): string | null {
  if (!ip) return "Please enter your server IP address";
  if (!IP_HOSTNAME_RE.test(ip)) return "That doesn't look like a valid IP address";
  return null;
}

```

Import these validators in custom CLI commands or UI components rather than duplicating regex patterns. For SSH payload validation, use `validateSshPayload` from the same module:

```typescript
import { validateSshPayload } from "@repo/onboarding";

function handleUserInput(payload) {
  const err = validateSshPayload(payload);
  if (err) {
    console.error(err);
    process.exit(1);
  }
  // Continue with verified payload…
}

```

## Configuring Environment and Deployment Targets

Environment variables drive platform behavior:

- **`CLOUD_MODE`** (`true`/`false`) – Forces cloud runtime, overriding other settings
- **`DEPLOY_MODE`** (`docker`/`bare`/`cloud`/`desktop`) – Selects the runtime and target combination

The resolution order is documented in [`packages/adapters/docs/ARCHITECTURE.md`](https://github.com/oblien/openship/blob/main/packages/adapters/docs/ARCHITECTURE.md). The platform factory reads these variables during `initPlatform()` to instantiate the correct adapter combination. Never hardcode deployment-specific logic; instead, rely on the factory to inject the appropriate runtime and infra providers.

## Maintaining Code Quality and Testing Standards

Openship enforces strict development standards across all workspaces:

- **Strict TypeScript** – All packages compile with `strict: true`, `noImplicitAny`, and `exactOptionalPropertyTypes`
- **Unit tests** – Co-locate tests with source code using the `*.test.ts` naming convention
- **Integration tests** – Located under `apps/api/test/modules`
- **Conventional commits** – Follow the Conventional Commits specification (e.g., `feat: add new service`)
- **Formatting** – Run `bun format` before committing to ensure Prettier compliance

Use `bun dev:<workspace>` (e.g., `bun dev:api`) to spin up individual workspaces during development. This conserves resources while allowing targeted testing of specific components.

## Summary

- Respect the three-layer architecture (runtime, infra, system) and compose components through the platform factory in [`packages/adapters/src/platform.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/platform.ts)
- Add database tables using Drizzle ORM schemas in `packages/db/src/schema/` with corresponding repositories in `packages/db/src/repos/`
- Reuse validation utilities from [`packages/onboarding/src/validation.ts`](https://github.com/oblien/openship/blob/main/packages/onboarding/src/validation.ts) to maintain consistent input handling across CLI and web interfaces
- Configure deployment targets via `CLOUD_MODE` and `DEPLOY_MODE` environment variables rather than hardcoding environment checks
- Maintain strict TypeScript compliance and conventional commit standards across all PNPM workspaces

## Frequently Asked Questions

### How does the openship platform factory work?

The platform factory in [`packages/adapters/src/platform.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/platform.ts) exports `createPlatform()`, which instantiates the appropriate combination of runtime, infra, and system adapters based on the `PlatformConfig` target. It returns either `createCloudPlatform()`, `createDesktopPlatform()`, or `createSelfHostedPlatform()`, ensuring the correct providers are injected for the specific deployment environment while keeping unused implementations tree-shaken from the bundle.

### Where should I add new database tables in openship?

Create a new schema file under `packages/db/src/schema/` using Drizzle ORM's `pgTable` definitions, then add a corresponding repository file under `packages/db/src/repos/` that exports typed query helpers. Follow the existing pattern of co-locating unit tests (e.g., [`new-entity.repo.test.ts`](https://github.com/oblien/openship/blob/main/new-entity.repo.test.ts)) to ensure data access logic remains tested and maintainable.

### How do I validate user input consistently across openship packages?

Import validation functions from [`packages/onboarding/src/validation.ts`](https://github.com/oblien/openship/blob/main/packages/onboarding/src/validation.ts), which exports utilities like `validateServerAddress()` and `validateSshPayload()`. These functions enforce regex patterns and security checks used by both the CLI and web dashboard, ensuring consistent error messaging and input sanitization across all user interfaces.

### What is the difference between runtime and infra layers in openship?

The **runtime** layer (`packages/adapters/src/runtime/`) manages container and process lifecycles (build, start, stop), while the **infra** layer (`packages/adapters/src/infra/`) handles external routing and SSL certificate provisioning. Runtime adapters know how services execute, whereas infra providers know how to expose them to the network, maintaining separation between execution and connectivity concerns.