Architecture
Design decisions and technical implementation
Gibil is a thin orchestration layer between you and your cloud provider. There's no custom infrastructure, no proprietary runtime, no vendor lock-in. It's a CLI that makes the right API calls in the right order. Today's supported providers are Hetzner Cloud and Vultr, both behind a single CloudProvider interface.
Design decisions
Raw fetch over provider SDKs
Zero extra dependencies, full control over the HTTP layer, easy to mock in tests at the fetch boundary. Each provider integration is one file with a private request<T>() method — no SDK churn, no transitive deps.
CloudProvider interface + ProviderRegistry
The CloudProvider interface is the abstraction boundary. Hetzner and Vultr both implement it. Every command and the MCP server route through ProviderRegistry, which resolves the right provider for each operation — including months later, because every instance metadata file carries its provider name. Nothing outside src/providers/ imports a specific provider class.
Adding a new cloud is one new file plus one row in the static catalog.
Static catalog
Provider metadata — sizes, default region, label — lives in plain TypeScript at src/providers/catalog.ts, separate from the provider class itself. That's how gibil providers describes a cloud without instantiating one (no token required to list options), and how the size abstraction (small / medium / large) stays consistent across clouds while mapping to the right native SKU per provider.
Cloud-init over SSH provisioning
The server configures itself on boot. Gibil generates a cloud-init script with your runtime, services, and tasks, then hands it to the provider. No SSH connection needed during setup, no fragile multi-step provisioning over the network. The script is provider-agnostic.
Auth is optional
The CLI works with just a provider token. Gibil's auth layer (Supabase + Stripe) adds metering and billing for the managed service, but it's entirely optional. BYOC users never touch it.
InstanceStore with dependency injection
Instance metadata is stored as JSON files in ~/.gibil/instances/. The store accepts a baseDir parameter — production uses ~/.gibil, tests use a temp directory. No global state, no singletons. Each metadata file carries its provider name so destroy/run/ssh resolve the correct cloud months after creation.
Tech stack
| Component | Technology | Why |
|---|---|---|
| CLI framework | Commander.js | Standard, well-documented |
| Build | tsup | Fast, zero-config ESM bundling |
| Language | TypeScript (strict) | Type safety without ceremony |
| SSH | ssh2 | Native Node.js SSH, no shelling out |
| Config | YAML (.gibil.yml) | Human-readable, version-controllable |
| Tests | Vitest | Fast, ESM-native, good DX |
| Cloud (EU/US) | Hetzner Cloud API | Cheapest full VMs, clean API |
| Cloud (APAC) | Vultr API | Tokyo, Seoul, Singapore, Sydney, Mumbai |
| Auth/billing | Supabase + Stripe | Optional, no extra hosting needed |
Project structure
src/
cli/
commands/ # One file per command (create, ssh, run, destroy, providers, ...)
index.ts # Entry point — registers all commands with Commander
providers/ # CloudProvider implementations + registry + static catalog
registry.ts # ProviderRegistry: resolves the right provider per command/instance
catalog.ts # Static metadata (sizes, regions) — no token needed
hetzner.ts # Hetzner provider
vultr.ts # Vultr provider
config/ # YAML parser + cloud-init script generator
ssh/ # Key generation + remote command execution
types/ # Shared TypeScript types (index.ts)
utils/ # Logger, store, auth, paths, validate, randomEach command is a single file with a registerXCommand(program) function. Providers are resolved through the registry, never imported directly. Utilities are small and single-purpose.
Next steps
- How It Works — the server lifecycle
- CLI Reference — command documentation