No description
  • Go 94.5%
  • Shell 2.6%
  • HTML 1.6%
  • Nix 0.7%
  • JavaScript 0.4%
  • Other 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Bradley Nelson 92d4eb78bd
Some checks failed
ci / no-committed-secrets (push) Successful in 14s
ci / nix-build (push) Successful in 2m1s
release / changes (push) Successful in 31s
release / publish (push) Has been skipped
ci / go-build (push) Successful in 2m50s
ci / lint (push) Successful in 3m55s
ci / go-test (push) Failing after 4m47s
docs: AGENTS.md — the cloud is deployed
prod (infra/Pulumi.prod.yaml) is live on homefirst.cloud: the first deploy
served Let's Encrypt staging certificates and the box now uses the production
CA. Cloud accounts are on main, no longer a side branch.
2026-10-04 15:04:42 -06:00
.forgejo/workflows infra: a $100/month budget and Google's billing stop-loss 2026-10-02 20:45:19 -06:00
docs servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
infra servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
internal servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
nix devenv: current Pulumi, gcloud and esc for deploys 2026-10-02 17:18:51 -06:00
scripts devenv: current Pulumi, gcloud and esc for deploys 2026-10-02 17:18:51 -06:00
.envrc Bootstrap homefirst-cloud control-plane skeleton 2026-06-23 23:06:12 -06:00
.gitignore Bootstrap homefirst-cloud control-plane skeleton 2026-06-23 23:06:12 -06:00
accounthost.go servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
accounthost_test.go account: closed sign-ups (default), admin page and invites 2026-10-03 10:14:41 -06:00
AGENTS.md docs: AGENTS.md — the cloud is deployed 2026-10-04 15:04:42 -06:00
devenv.nix infra: pulumi up delegates the domain at Porkbun to the Cloud DNS zone 2026-10-02 18:41:42 -06:00
Dockerfile edge: HTTPS and gRPC on one TLS port, certificates from CertMagic (DNS-01 on Cloud DNS) 2026-10-01 00:59:40 -06:00
enroll.go servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
enroll_test.go servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
flake.lock devenv: current Pulumi, gcloud and esc for deploys 2026-10-02 17:18:51 -06:00
flake.nix servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
go.mod servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
go.sum servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
justfile servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
LICENSE Bootstrap homefirst-cloud control-plane skeleton 2026-06-23 23:06:12 -06:00
main.go servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
main_test.go servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
README.md servers and accounts: install → server everywhere, on connect v0.4.0 2026-10-03 22:39:33 -06:00
secretfiles.go main: Stripe billing settings; billing gates linking on the account host 2026-10-03 02:16:47 -06:00
secretfiles_test.go app: per-install keys, signed assertions and encrypt-to-home 2026-10-02 01:47:38 -06:00
VERSION repo: move to Forgejo, and consume the contract as a tagged module 2026-09-27 13:26:12 -06:00

homefirst.cloud

The hosted control plane for Home First. It implements the server side of the packages/connect contract for the self-hosted apps (tendant, openPRM): a home server dials out one connection, and the cloud acts as its public address for third-party events — it registers webhooks/push subscriptions with providers (Gmail, Slack, GitHub, …) and proxies what they send down to the home.

License: AGPL-3.0 (see LICENSE). Hosting: developed in the open at git.bcnelson.dev/home-first/home-first-cloud, with a read-only mirror on Codeberg. Design: the full hosting & protocol plan is in docs/CLOUD-PLAN.md; cloud accounts in docs/ACCOUNTS-PLAN.md.

Design in one paragraph

One small always-on box (GCP e2-micro, single Go container) terminates TLS and holds each home's outbound gRPC connection. The cloud is a dumb event proxy: providers call hooks.homefirst.cloud/h/<id>, the cloud checks the provider's signature (it holds every secret, since it makes every subscribe call), acks, buffers, and forwards raw headers + body plus the verification result to the home, which feeds its own intake. The cloud also makes the provider-side subscribe/renew calls using short-lived access tokens the home lends per call — refresh tokens never leave the home. State (endpoints, subscriptions, the event buffer) lives in CockroachDB Serverless; each copy keeps the streams it holds in memory and agrees with the others through the database, so several copies can run behind a load balancer (the box runs one today). Remote access is out of v1.

Status — Slack events (build-order step 6, cloud side)

The cloud serves the v1 gRPC contract (homefirst.connect.v1, defined in the monorepo's packages/connect), the public webhook ingress and the OAuth redirect for Home First's shared provider registrations:

  • One TLS port in production (HOMEFIRST_CLOUD_TLS=acme): HTTPS and gRPC share :8443 (the box publishes it as hooks.homefirst.cloud:443), routed by content-type: application/grpc; the certificate comes from CertMagic in-process (ACME DNS-01 against Google Cloud DNS, credentials from the box's service account); plain :8080 only answers /healthz and redirects to HTTPS. Without HOMEFIRST_CLOUD_TLS (development, tests) it is plaintext HTTP on :8080 and h2c gRPC on :8081. Settings: main.go's header and internal/edge.
  • gRPC: Register with version negotiation and sunset signalling, server auth on every RPC, the Session stream (heartbeats, delivery + ack, replay of un-acked deliveries on reconnect), Subscribe/Renew/Unsubscribe through the provider registry, and StartOAuth / MintToken (refresh) for Google, Slack and Linear. Call is still Unimplemented (step 7).
  • HTTP on :8080: GET|POST /h/<endpoint id> — the provider-facing ingress (signature checked in the cloud, buffered, acked, forwarded raw to the home; /h/gmail is Gmail's one shared Pub/Sub push endpoint, routed by mailbox; /h/slack is the Slack app's one Events API Request URL, fanned out to every subscribed user who can see an event and deduped on Slack's retries), GET /oauth/<provider>/callback — the one redirect URI each provider registration lists (the cloud exchanges the code and hands the tokens to the home over its session; it stores no user token), GET /healthz → ok (liveness), GET /readyz (readiness: 503 while draining or without the database), GET /version → release + served protocol majors.
  • Cloud accounts (branch accounts, not deployed; docs/ACCOUNTS-PLAN.md): with HOMEFIRST_CLOUD_ACCOUNT_URL the same listeners also serve the account host account.homefirst.cloud, split by Host — sign-up and sign-in (email + password, Google, passkeys), linking a server to an account (gRPC StartLink / PollLink / Unlink, approved at /link), per-server grants enforced on every RPC (NEEDS_GRANT with an approve URL), and optional Stripe billing (Checkout, Customer Portal, webhook at /billing/webhook). Its settings are in accounthost.go's header.
  • Providers: Linear (webhooks + OAuth); Google (OAuth) with the push providers gmail (users.watch to Home First's Pub/Sub topic; the push OIDC token is verified against Google's JWKS; enabled by HOMEFIRST_CLOUD_GMAIL_TOPIC + _GMAIL_PUSH_SERVICE_ACCOUNT), google-calendar and google-drive (notification channels with a per-subscription channel token); Slack (OAuth, and with HOMEFIRST_CLOUD_SLACK_SIGNING_SECRET the Events API: slack subscriptions per user, url_verification, tokens_revoked / app_uninstalled routed to the affected homes; the app manifest is docs/slack-app-manifest.yaml). Home First's client credentials come from HOMEFIRST_CLOUD_<PROVIDER>_CLIENT_ID / _CLIENT_SECRET; a provider without them is "not configured".
  • Renewal: a sweeper asks the owning home (RenewalDue) to Renew expiring subscriptions with a fresh token, marks unrenewed ones lapsed (and says so with a past expires_at), and tells offline homes on their next connect. Renew renews at the provider only when due (lapsed, or inside the same window); the home's Renew after every reconnect otherwise just checks ownership and, for Gmail/Slack, the token's account — no channel churn.
  • State in CockroachDB/Postgres (DATABASE_URL): endpoints, subscriptions, AES-GCM-encrypted provider secrets, the event buffer (72 h TTL by default), pending OAuth authorizations (10 min, single use, PKCE verifier sealed) and an audit log. Without DATABASE_URL it runs in memory (dev only, one copy).
  • Replica-aware: two or more copies can serve homes together on one database. Session ids are signed tokens (Register and the Session stream may hit different copies), one session per server across copies (newest wins), an event or notice for a home on another copy rings that copy, which polls for it within about a second (an idle poll is one cheap read), an OAuth callback on the wrong copy hands the sealed, single-use code to the copy holding the session, the sweeps run under DB leases, SIGTERM drains (homes reconnect elsewhere; GET /readyz fails), and CertMagic can keep its data in the database (HOMEFIRST_CLOUD_TLS_STORAGE=db). Design, costs and limits: CLOUD-PLAN.md "Running more than one copy".

Servers authenticate with their own keys (security phase 1): an operator issues a one-time enrollment code (serverIds in infra/, or homefirst-cloud enroll-code <server-id> on the box), the home spends it once to register its public keys, and then signs every RPC; webhook payloads and OAuth tokens are sealed to the home before the cloud keeps or sends them (CLOUD-PLAN.md "Server authentication and encrypt to home", "Security roadmap"). The full environment is documented at the top of main.go. Next (see AGENTS.md and the design doc): TLS (CertMagic) and mobile push.

Develop

direnv allow         # provisions the Go + buf/protobuf toolchain (devenv)
just build           # CGO_ENABLED=0 go build ./...
just test            # go test -race ./...
just dev             # run locally → HTTP /healthz + /version + /h/<id> on :8080, gRPC on :8081

Locally with TLS (one port, like production) using a certificate from files, e.g. from mkcert localhost:

HOMEFIRST_CLOUD_TLS=files HOMEFIRST_CLOUD_TLS_CERT_FILE=localhost.pem \
HOMEFIRST_CLOUD_TLS_KEY_FILE=localhost-key.pem go run .   # HTTPS + gRPC on :8443

A home connects with HOMEFIRST_CLOUD_ADDR=hooks.homefirst.cloud:443 (TLS, system roots; never HOMEFIRST_CLOUD_INSECURE against the real cloud), HOMEFIRST_SERVER_ID and HOMEFIRST_CLOUD_TOKEN. Deploying: infra/README.md "First deploy — checklist".

The SQL store tests (and the SQL variants of the end-to-end and multi-copy tests — internal/server/cluster_test.go runs two and three copies on one database) need a database; they skip without one. Point TEST_DATABASE_URL at any Postgres or CockroachDB server you can create databases on — each test makes and drops its own:

TEST_DATABASE_URL='postgres://root@127.0.0.1:26257/defaultdb?sslmode=disable' just test   # cockroach start-single-node --insecure

For a throwaway Postgres, listen on TCP only (a unix socket path under a deep scratch directory can exceed the 107-byte limit): pg_ctl -D <dir> -o "-c listen_addresses=127.0.0.1 -p 55433 -c unix_socket_directories=''" start.

The connect contract is an ordinary tagged module dependency (git.bcnelson.dev/home-first/homefirst/packages/connect), resolved straight from Forgejo — so this repo builds on its own, with no sibling checkout required.

Evolving the contract is therefore a two-step: land the change in the monorepo's packages/connect, tag it (packages/connect/vX.Y.Z), then bump the require here. For day-to-day co-development, add a temporary replace pointing at a local checkout and drop it before committing:

go mod edit -replace git.bcnelson.dev/home-first/homefirst/packages/connect=../homefirst/packages/connect
# ... iterate ...
go mod edit -dropreplace git.bcnelson.dev/home-first/homefirst/packages/connect

CI & releases

CI is Forgejo Actions (.forgejo/workflows/), on the self-hosted romeo runner:

  • ci.yml — vet + build both modules, lint, go test -race, on every push/PR.
  • release.yml — builds the multi-arch image from the flake (nix dockerTools, no Docker daemon — job containers get no docker socket) and pushes it to Forgejo's registry as git.bcnelson.dev/home-first/home-first-cloud. A push to main publishes :latest; a YYYY.M.NN tag publishes that version plus its channel. Needs repo secrets REGISTRY_USER / REGISTRY_TOKEN (a Forgejo token with scope write:package, held by a user with package write on the org).

Run the workflows locally before pushing with just ci-local (see scripts/ci-local.sh); just images builds the release images the way CI does.