Skip to content

lobs-ai/perch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

perch

Local-first hosting for prototypes. Drop a project in a folder, run one command, and get a real HTTPS URL with TLS, single sign-on, and secrets injected — without ever touching DNS configs, cert warnings, OAuth client registrations, or .env sprawl.

Status: working. The spine, secrets, auth, all three reaches (private/tailnet/public), scaffolding, an admin dashboard, and an interactive CLI are built — see the roadmap at the bottom. This README is the canonical design; AGENTS.md is the working contract for anyone (human or agent) building it.


The problem

Spinning up a web prototype carries a recurring tax: pick a port, wire a reverse proxy, fight a self-signed cert, register a new OAuth app for "log in with Google," stash API keys in an untracked .env, and remember which thing is running where. Every new idea pays it again. perch pays it once and makes every new site a single word of config.

The dream

cd ~/perch/whatever
perch up                          # → https://whatever.perch.test  (TLS, behind your SSO, secrets injected)

perch new dashboard --template next   # scaffold, already wired to the platform
perch publish demo                # → public at https://demo.perch.dev, only you can log in
perch share demo --with amy@gmail.com # → public, allowlisted to you + Amy
perch ls                          # every app: status, URL, reach, access
perch logs whatever -f
perch start | stop | restart | status [app]

Subdomain defaults to the directory name. Stacks are autodetected (Node, Python, static, Docker). Zero config gets you a private HTTPS URL; one word changes who can reach it.

Two ways to drive it

Same control plane, two front doors — use whichever fits the moment.

  • The CLI. Every capability is a perch subcommand. New to it? Run perch i for an interactive menu: it lists your apps and lets you open, restart, publish, share, view logs, manage tokens, run the doctor, and edit secrets — no flags to memorise. Bare perch in a terminal drops straight into that menu. perch new prompts for a template when you don't name one.
  • The dashboard. perch dogfoods itself: a small admin home page is served at https://home.perch.test (owner-only, behind your own SSO). It's the same control plane the CLI talks to, with five tabs:
    • Overview — daemon health, how many apps are running, rolled-up traffic, and a setup-readiness summary (which reaches are good to go).
    • Apps — every app with live status and traffic; open/start/stop/restart, edit policy (reach, access, allowlist, reauth), tail logs, issue/revoke service tokens, or bring a new folder online.
    • Setup — the environment doctor, with a copy-paste fix command for anything not yet wired (perch never changes your system for you).
    • Secrets — manage the encrypted keystore (names only on read; values are write-only and never shown back or logged).
    • Guide — the reach × access model, a quickstart, and a CLI cheatsheet.

Core concept: policy = reach × access

There aren't three hosting stacks — there's one control plane, and every app carries a policy that selects the substrate. Reach is how far it's exposed; access is who gets through.

Reach — where it's announced and how traffic arrives:

reach name DNS cert exposure
private name.perch.test dnsmasq → 127.0.0.1 mkcert Caddy loopback listener
tailnet name.perch.ts.net Tailscale MagicDNS tailscale cert Caddy on the tailnet
public name.perch.dev Cloudflare TLS at CF edge cloudflared tunnel → Caddy

Access — who gets through, enforced at the edge:

access mechanism
me Google OAuth, email == yours
allowlist Google OAuth, email ∈ allow[]
tailnet Tailscale device/user identity — no login prompt
service-token signed bearer token, for machines/CLIs
open no auth (must be explicit, never a default)

Safe by default: access is derived from reach unless you override it — privateme, tailnettailnet, publicme. So reach = "public" alone gives you a public URL that only you can log into. Opening or sharing it is always a separate, explicit change.

Per-app config: perch.toml

Drop this in any project root. Everything is optional; the CLI verbs (publish, share, private) edit it for you.

name    = "demo"               # subdomain; default = directory name
run     = "npm run dev"        # start command; default = autodetected
port    = 0                    # 0 = perchd assigns a free loopback port via $PORT

reach   = "private"            # private | tailnet | public          (default: private)
bindings = ["private","tailnet"] # optional: serve on several reaches at once (multi-binding)
access  = "me"                 # me | allowlist | tailnet | service-token | open
allow   = ["amy@gmail.com"]    # emails, when access = allowlist
reauth  = ""                   # e.g. "5m" — force fresh Google re-auth (step-up for sensitive apps)
open_paths = ["/webhooks/*"]   # paths exempt from auth — you verify the provider signature yourself

secrets = ["openai", "stripe"] # secret scopes injected as env at start

Architecture

   private              tailnet                public
 *.perch.test        *.perch.ts.net          *.perch.dev
 (dnsmasq +          (MagicDNS +            (Cloudflare DNS,
  mkcert)             Tailscale cert)        TLS at CF edge)
      │                   │                       │
      │                   │                 cloudflared tunnel
      ▼                   ▼                       ▼
 ┌────────────────────────────────────────────────────────┐
 │              Caddy  (embedded in perchd)                  │
 │  • host-based routing        • per-app TLS              │
 │  • forward-auth ─► oauth2-proxy ─► Google OIDC          │
 │  • Tailscale whois for tailnet identity                 │
 │  • strips inbound X-Perch-*, injects the verified one    │
 └───────────────────────────┬────────────────────────────┘
          authed request + X-Perch-Email + secrets (env)
                              ▼
                  127.0.0.1:PORT   (supervised app)
                              ▲
 ┌────────────────────────────┴───────────────────────────┐
 │  perchd control plane                                    │
 │  registry (sqlite) · policy compiler · supervisor       │
 │  (process/Docker) · secrets (SOPS+age) · programs       │
 │  Caddy routes in-process                                │
 └─────────────────────────────────────────────────────────┘

Two components:

  • perch — the single CLI. Lifecycle (start/stop/restart/status) plus new, up, down, restart <app>, rm, publish, share, private, bind, secrets, token, ls, logs, open, doctor, backup, and i (interactive menu).
  • perchd — the daemon. Embeds Caddy as a library, so it is the proxy and the control plane in one binary: app registry, policy→mechanism compiler, process/Docker supervisor, secrets resolver/injector, Caddy route programmer, traffic metrics, and the in-process dashboard served at home.perch.test.

How identity is confirmed

You never check a password yourself. You delegate to an identity provider that already did, then verify its signed proof — and the app behind the edge trusts a header it can't be lied to about. Full detail and the non-negotiable invariants live in AGENTS.md; the short version:

  • Humans (web): Google OIDC via oauth2-proxy. Verify the signed ID token (signature against Google's JWKS, plus iss/aud/exp/nonce, require email_verified), set a signed+encrypted cookie scoped to the parent domain → SSO across every subdomain.
  • Your devices (tailnet): Tailscale/WireGuard authenticates every packet at the network layer; Caddy maps the source to a user via whois. No prompt.
  • To the app: Caddy injects X-Perch-Email. The app trusts it only because (a) apps bind 127.0.0.1 so nothing but Caddy can reach them, and (b) Caddy strips any client-supplied X-Perch-* first. This is the #1 thing to get right.
  • Machines: signed service-token, or exempt a webhook path via open_paths and verify the provider's own signature.

Bootstrap once, declare per-app

The painful infra is a one-time perch init / perch doctor:

  • dnsmasq + /etc/resolver/perch.test (wildcard *.perch.test → 127.0.0.1)
  • mkcert -install + a *.perch.test wildcard cert
  • tailscale up (for tailnet reach)
  • a domain (perch.dev) + Cloudflare account + cloudflared login + named tunnel (for public reach)
  • one Google OAuth client, brokered to all subdomains via oauth2-proxy
  • an age keypair for SOPS

After that, a new site is one perch up and at most one word of reach/access.

Roadmap (v0)

All four pains land in v0, sequenced on a working spine. Through M10 this is built:

  1. Spineperchperchd, registry, autodetect, supervisor, embedded Caddy → private reach end-to-end (cd && perch up → https://name.perch.test).
  2. Secrets — SOPS+age resolution and env injection in the supervisor.
  3. Auth — oauth2-proxy + Google OIDC forward-auth + the access model (me/allowlist/service-token/reauth).
  4. Reachtailnet (Tailscale) and public (cloudflared), plus multi-binding (perch bind).
  5. Scaffoldperch new templates, pre-wired to the platform.
  6. Dashboard & observability — access-log metrics + the admin home page at home.perch.test.
  7. Polish — lazy start, idle-out, reauth, backup/restore.
  8. Ease of use — the multi-tab admin dashboard (Overview/Apps/Setup/Secrets/Guide) and the interactive CLI (perch i), so you can drive everything without memorising flags.

Built on (not reinvented)

The proxy, certs, and auth are solved problems. perch is the thin glue + conventions that wire them into one command: Caddy (proxy + TLS, embedded), oauth2-proxy + Google OIDC (web identity), Tailscale (device identity + cross-machine DNS), cloudflared (public exposure), mkcert + dnsmasq (local TLS + DNS), SOPS + age (secrets). The custom code is the control plane, the policy model, and the DX.

Stack

Go, with Caddy embedded as a library. SQLite for the registry. See AGENTS.md for layout, conventions, and build commands.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors