Skip to main content

Overview

rs-tunnel is organized as a pnpm workspace monorepo using Turborepo for task orchestration. The repository contains applications, shared packages, and configuration.

Workspace Configuration

The workspace is defined in pnpm-workspace.yaml:
pnpm-workspace.yaml
packages:
  - apps/*
  - packages/*
This structure enables shared dependencies, consistent tooling, and efficient builds across all packages.

Directory Structure

Apps Directory

The apps/ directory contains the deployable applications:

apps/api

@ripeseed/apiFastify-based REST API that handles:
  • User authentication via Slack OAuth
  • Cloudflare tunnel lifecycle management
  • DNS record creation/deletion
  • Tunnel lease management and heartbeats
  • Background cleanup worker for stale tunnels
Stack: Fastify, Drizzle ORM, PostgreSQL, JWT

apps/cli

@ripeseed/rs-tunnelCommand-line interface for users:
  • login - Authenticate with API via Slack
  • up - Start tunnel with ngrok-style dashboard
  • list - View active tunnels
  • stop - Stop tunnel and cleanup DNS
  • logout - Clear authentication
  • doctor - Diagnose connection issues
Stack: Commander.js, Node.js HTTP proxy

Packages Directory

The packages/ directory contains shared libraries:

packages/shared

@ripeseed/sharedShared TypeScript contracts and Zod schemas consumed by both API and CLI:
  • Request/response types
  • Validation schemas
  • API contract definitions
  • Domain models
Published to npm for external consumption

packages/config

@ripeseed/configShared configuration for development tools:
  • ESLint configuration
  • Prettier configuration
  • TypeScript base config
  • Vitest setup
Private package - not published

Dependency Graph

Build Order Matters: The @ripeseed/shared package must be built before api or cli can typecheck or build. Turborepo handles this automatically via the ^build dependency.

Turborepo Pipeline

The build pipeline is defined in turbo.json:
turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "test": {
      "dependsOn": ["^test"],
      "outputs": ["coverage/**"]
    },
    "typecheck": {
      "dependsOn": ["^typecheck", "^build"]
    },
    "format": {
      "cache": false
    }
  }
}

Pipeline Behavior

  • Dependencies: ^build (build dependencies first)
  • Outputs: dist/**, .next/**
  • Cached: Yes
Ensures packages are built in dependency order. The @ripeseed/shared package builds first, then api and cli.

Package Scripts

Each package defines standard npm scripts. From the repository root:

Root-level commands

# Run tasks across all packages
pnpm build        # Build all packages
pnpm dev          # Run all apps in dev mode (parallel)
pnpm lint         # Lint all packages
pnpm test         # Test all packages
pnpm typecheck    # Type-check all packages
pnpm format       # Check formatting

Package-specific commands

# Target specific package
pnpm --filter @ripeseed/api dev
pnpm --filter @ripeseed/rs-tunnel build
pnpm --filter @ripeseed/shared test

# Run command in package directory
pnpm --filter @ripeseed/api db:migrate
pnpm --filter @ripeseed/rs-tunnel exec tsx src/index.ts

Workspace Dependencies

Packages reference each other using workspace protocol:
apps/cli/package.json
{
  "dependencies": {
    "@ripeseed/shared": "workspace:*"
  },
  "devDependencies": {
    "@ripeseed/config": "workspace:*"
  }
}
The workspace:* protocol ensures packages always use the local workspace version during development. During publish, pnpm automatically replaces this with the actual version number.

Build Output

All packages compile TypeScript to the dist/ directory:
packages/shared/
├── src/
│   ├── index.ts
│   └── contracts.ts
└── dist/              # Build output
    ├── index.js
    ├── index.d.ts
    ├── contracts.js
    └── contracts.d.ts
The project uses ESM (ECMAScript modules) exclusively. All imports use .js extensions, even in TypeScript source files:
import { validateRequestedSlug } from '../../src/utils/slug.js';

Publishing Strategy

Two packages are published to npm:
  1. @ripeseed/shared - Public package with shared contracts
  2. @ripeseed/rs-tunnel - Public CLI package
The API (@ripeseed/api) is private and deployed as a service.

Package Metadata

packages/shared/package.json
{
  "name": "@ripeseed/shared",
  "version": "0.1.2",
  "private": false,
  "publishConfig": {
    "registry": "https://registry.npmjs.org/",
    "access": "public"
  },
  "files": ["dist"]
}

Next Steps

Local Setup

Get the development environment running

Testing

Learn about the testing strategy

Contributing

Guidelines for contributing code

Release Process

How packages are published to npm

Build docs developers (and LLMs) love