No description
  • Go 70.3%
  • Nix 17.8%
  • HTML 11.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Owen Phillips b112a4621f Add pepela: ButterflyMX door opener with pocket-id OIDC
Populate the scaffold with the Go app: auth/serve/doors subcommands, the
go-butterflymx client wrapper with refresh-token persistence, in-app
pocket-id OIDC gating on a door_users group, the web UI, and Nix packaging
(flake + buildGoModule with go_1_25/GOEXPERIMENT=jsonv2 + a hardened NixOS
module).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 00:25:39 -07:00
internal Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
nix Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
.gitignore Initial commit 2026-09-02 23:00:43 -07:00
flake.lock Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
flake.nix Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
go.mod Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
go.sum Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
LICENSE Initial commit 2026-09-02 23:00:43 -07:00
main.go Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00
README.md Add pepela: ButterflyMX door opener with pocket-id OIDC 2026-09-04 00:25:39 -07:00

pepela

პეპელა (Georgian for butterfly) — a small self-hosted web app to open ButterflyMX doors, gated behind pocket-id OIDC.

It's a spiritual clone of ButterflyMX-444, built on the reverse-engineered Go client go-butterflymx instead of the Python library. A logged-in, authorized user sees the building's access points and taps one to release the door.

Unofficial and unsupported: it drives reverse-engineered ButterflyMX endpoints and can break whenever they change. Use at your own risk.

How it works

  • Holds a ButterflyMX OAuth2 refresh token (never your password) and re-mints the short-lived Rails API token on demand.
  • Enforces login in-app as an OIDC client of pocket-id, requiring a configurable group (door_users by default) in the groups claim.
  • Stateless sessions (in-memory), CSRF-protected unlock, audit-logged opens.

Build

Requires Go 1.25 with GOEXPERIMENT=jsonv2 (go-butterflymx uses encoding/json/v2).

GOEXPERIMENT=jsonv2 go build .
# or:
nix build

Commands

pepela auth — one-time login

Runs the interactive ButterflyMX OAuth2 + PKCE flow (handles 2FA via the real login page) and prints the token JSON, including the refresh token:

pepela auth > token.json

Follow the printed URL, log in, and paste back the com.butterflymx.oauth://… URL you're redirected to. Store token.json as the app's token file (e.g. in agenix).

pepela doors — verify the token

Lists the account's access points using only BUTTERFLYMX_TOKEN_FILE (no OIDC, no persistence). A quick way to confirm pepela auth produced a working token — it forces a live API call, so it also exercises the refresh path:

BUTTERFLYMX_TOKEN_FILE=~/pepela-token.json pepela doors

pepela serve — run the server

Configured entirely via environment variables:

Variable Required Default Meaning
PEPELA_BASE_URL yes Public URL, used to build the OIDC redirect (<base>/auth/callback)
BUTTERFLYMX_TOKEN_FILE yes Seed token JSON from pepela auth
OIDC_ISSUER yes pocket-id issuer URL
OIDC_CLIENT_ID yes OIDC client ID
OIDC_CLIENT_SECRET_FILE yes File containing the OIDC client secret
OIDC_REQUIRED_GROUP no door_users Group required to open doors
PEPELA_LISTEN no 127.0.0.1:3005 Bind address
STATE_DIRECTORY no . Where the rotated token is cached (set by systemd)

The seed token file is read-only; rotated tokens are cached to $STATE_DIRECTORY/token.json so refresh-token rotation survives restarts. Bind to loopback and only expose the app through a TLS-terminating reverse proxy.

NixOS

This flake ships nixosModules.default (services.pepela) and packages.default. Example:

services.pepela = {
  enable = true;
  baseUrl = "https://door.example.com";
  tokenFile = config.age.secrets.butterflymx-oauth-token.path;
  oidc = {
    issuer = "https://id.example.com";
    clientId = "…";               # from the pocket-id admin UI
    clientSecretFile = config.age.secrets.pocket-id-oauth2-pepela-secret.path;
    requiredGroup = "door_users";
  };
};

Create the OIDC client in pocket-id with redirect https://door.example.com/auth/callback, and put your users in the door_users group.

License

GPL-3.0-only (go-butterflymx is GPL-3.0).