trustless 0.4.1

HTTPS on registrable domains for local development without touching your system trust store
Documentation
# trustless

HTTPS on registrable domains for local development -- without touching your system trust store.

```
trustless run rails server
# -> https://my-project.dev.example.com:1443
```

Trustless infers a subdomain from your project (directory name, package.json, or `.trustless.json`), allocates an ephemeral port, and starts your app behind a local HTTPS proxy -- all with a single command.

## Why

Tools like [Portless](https://github.com/vercel-labs/portless) solve the port-number problem by giving each dev server a stable `.localhost` URL. But `.localhost` is not a registrable domain, so:

- **Same-site cookies don't work.** `a.localhost` and `b.localhost` are independent sites, not subdomains of a common registrable domain, so `SameSite` cookies can't be shared between services
- **Secure context requires HTTPS with trusted certificates.** Browsers grant secure context to plain `localhost`, but once you use a registrable domain, you need a real TLS certificate
- **Self-signed certs need trust store changes.** Installing a local CA means modifying system or browser trust stores on every developer's machine and poses security risks if not handled carefully.

Trustless fixes this by sharing a publicly trusted certificate through a key provider you deploy once, then every developer on the team gets HTTPS on registrable domains with zero local trust store changes -- without distributing private keys.

__It's noteworthy that sharing a private key is risky!__ Trustless never distributes raw private keys, key providers expose only a signing interface. But users must acknowledge its risk. We can tolerate this by minimizing the risk: by placing a key provider between a locally running HTTPS proxy and the actual key materials, we can instantly revoke access when needed. Raw key export is disallowed.

## Install

- **Mise:** `mise use -g 'github:sorah/trustless[version=latest,version_prefix=rel/]'`
- **Binaries:** Download from [GitHub Releases]https://github.com/sorah/trustless/releases?q=rel%2F&expanded=true

_These commands install all executables provided in this repository (`trustless` + provider plugin commands)._

## Quick Start

### 1. Set up a key provider

The easiest option for solo use is the [Filesystem Provider](docs/filesystem-provider.md) -- point it at a directory containing your wildcard certificate and key.

For team use with access control and instant revocation, see the [AWS Lambda Provider](docs/lambda-provider.md). You can also [write your own](docs/writing-key-provider.md).

### 2. Setup DNS

Configure your wildcard domain (e.g. `*.dev.example.com`) to point to `127.0.0.1` and `::1` via DNS. Trustless does not alter DNS resolution on the machine in any way.

Choose a domain isolated from staging, production, and other in-house tools. Anyone with access to the key provider can sign TLS handshakes for its domains.

### 3. Configure trustless

```bash
trustless setup -- trustless-provider-lambda --function-name my-key-provider
```

Key providers are specified as an arbitrary command line, so you can use any provider implementation.

### 4. Run your app

```bash
cd my-api && trustless run rails server
# -> https://my-api.dev.example.com:1443

cd my-frontend && trustless run next dev
# -> https://my-frontend.dev.example.com:1443
```

The proxy auto-starts on first use. Start it explicitly with `trustless proxy start` if you prefer.

## Security Notice

> [!CAUTION]
> **You are sharing a private key.** Anyone with access to the key provider can sign TLS handshakes or any blobs such as JWT -- which means they can impersonate your services. Signing is the most important operation of an asymmetric key, and sharing it is inherently risky.
>
> Trustless reduces abuse risk compared to distributing raw key files by having a key provider concept: Access to signing can be revoked instantly by removing provider access, and the key itself is never exported. But while someone has access, they can fully impersonate.
>
> To limit the blast radius: **Use a dedicated domain exclusively for local development** (e.g. `*.lo.mycompany-dev.com`). You may want to have a isolated registered domain for this purpose, even avoiding subdomains.
> Never reuse certificates, keys, or domains that are used for other purposes, such as existing TLS private keys or domains serving real traffic.

## How It Works

```mermaid
flowchart TD
    Browser["Browser<br>https://my-api.dev.example.com:1443"]
    Proxy["trustless proxy<br>(TLS termination, port 1443)"]
    ProviderCmd["Provider Command<br>(local CLI)"]
    ProviderServer["Provider Server<br>(e.g. Lambda function;<br>signs only, no key export)"]
    Storage[("Key Storage<br>(e.g. S3)")]
    App1[":4123<br>my-api"]
    App2[":4567<br>my-frontend"]

    Browser -->|HTTPS| Proxy
    Proxy -.->|"sign request → signature"| ProviderCmd
    ProviderCmd -.-> ProviderServer
    ProviderServer --- Storage
    Proxy -->|HTTP| App1
    Proxy -->|HTTP| App2
```

1. **Key provider** -- You deploy a provider (e.g. AWS Lambda + S3) that holds a wildcard certificate for your dev domain. The provider signs TLS handshake data on request but never exports the private key.
2. **Proxy** -- `trustless proxy` runs locally, terminates TLS using the provider for signing, and forwards plain HTTP to your app on loopback.
3. **Run** -- `trustless run <command>` infers a subdomain, assigns an ephemeral port, registers a route with the proxy, and starts your app with `PORT` and `HOST` set.

## Routing

### Subdomain inference

`trustless run` infers a subdomain automatically: `.trustless.json`, `package.json` (name) in the project, Git repository name, or current directory name.

### Explicit subdomain with `trustless exec`

When you need a specific subdomain rather than the inferred one, use `trustless exec`:

```bash
trustless exec api rails server
# -> https://api.dev.example.com:1443

trustless exec web next dev
# -> https://web.dev.example.com:1443
```

When a provider has multiple wildcard domains, the first one is used by default. Use `--domain` to pick a specific one:

```bash
trustless exec api --domain=staging.example.com rails server
```

See [Routing and Running Apps](docs/routing.md) for full details on domain resolution, port allocation, environment variables, and route lifecycle.

## Profiles

Use multiple profiles when you have more than one key provider:

```bash
trustless setup --profile=another -- ...
trustless run --profile=another rails server
```

## Commands

```bash
trustless run [--profile=NAME] <command...>                 # Run app (auto-infer subdomain)
trustless exec <subdomain> [--profile=NAME] <command...>    # Run app (explicit subdomain)
trustless setup [--profile=NAME] -- <provider-command...>   # Save a provider to a profile
trustless proxy start                                       # Start the proxy
trustless proxy stop                                        # Stop a running proxy
trustless proxy reload                                      # Reload provider configuration (restarts all providers)
trustless list                                              # List active routes with URLs
trustless get <name>                                        # Print the URL for a named service
trustless status                                            # Show proxy status and providers
trustless route add <hostname> <backend>                    # Register a static route
trustless route remove <hostname>                           # Remove a route
trustless test-provider [--profile=NAME]                    # Verify a provider works
```

Aliases: `trustless l` = list, `trustless s` = status.

## Error Pages & Status

The proxy serves styled HTML error pages with dark mode support:

- **502 Bad Gateway** — when the backend is unreachable
- **508 Loop Detected** — when a forwarding loop is detected
- **404 Not Found** — when no route matches, showing a list of active routes

Visiting `https://trustless.<domain>/` in the browser shows a status page with active routes and provider health.

## Environment Variables

When using `trustless exec` or `trustless run`, the following environment variables are set for the child process:

- **`PORT`** — the ephemeral port your app should listen on
- **`HOST`** — the hostname to bind to (loopback)
- **`TRUSTLESS_URL`** — the full HTTPS URL for the service (e.g. `https://my-app.dev.example.com:1443`)

Set `TRUSTLESS=0` or `TRUSTLESS=skip` to bypass the proxy entirely and run the command without routing through trustless.

## Framework Detection

`trustless exec` and `trustless run` auto-inject framework-specific flags so your dev server listens on the correct host and port:

- **Vite** / **React Router** / **Astro**`--host` and `--port`
- **Angular**`--host` and `--port`
- **React Native** / **Expo**`--port`

## Prior Art

Heavily inspired by [vercel-labs/portless](https://github.com/vercel-labs/portless). Portless makes port numbers unnecessary -- hence _port-less._ Trustless extends the idea to registrable domains over HTTPS, removing the need to trust self-signed certificates or local CAs -- hence _trust-less._ No trust store modifications required.

## State Directory

`$XDG_RUNTIME_DIR/trustless` or `~/.local/state/trustless`