Self-hosted web app for cataloging lendable devices (drills, saws, etc.) and running a request → handover → return lending workflow with email notifications. Built on PocketBase embedded in a single Go binary; server-rendered UI with htmx, no Node.js/JS build step.
- An OIDC provider (tested against Authentik; any standard OIDC provider, e.g. Zitadel, should work).
- Docker and Docker Compose, for self-hosted deployment.
- Create an OAuth2/OIDC application/provider for this app.
- Set its redirect URI to
${BASE_URL}/oidc/callback(must matchBASE_URLexactly, including scheme and no trailing slash). - Add a groups scope mapping to the client so the
groupsclaim is included in the userinfo/id_token response — required for admin sync to work. Without this,OIDC_ADMIN_GROUPhas nothing to match against and no user will ever become an admin via login. - Note the issuer URL, client ID, and client secret for the next step.
Copy .env.example to .env and fill in:
| Variable | Required | Description |
|---|---|---|
OIDC_ISSUER |
yes | Your provider's issuer URL (its /.well-known/openid-configuration must be reachable at <issuer>/.well-known/openid-configuration) |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
yes | From your OIDC provider |
OIDC_ADMIN_GROUP |
no | Group name granting admin/cleanup rights; leave unset to disable admin sync |
BASE_URL |
yes | Public URL this app is reachable at, no trailing slash — must be https:// in any real deployment, see below |
SESSION_SECRET |
yes | Random string, 32+ characters, used to sign session cookies |
PUBLIC_READ |
no (default false) |
true to let anyone browse/view devices without logging in; requesting a device always requires login regardless |
DEV_AUTH |
no (default false) |
true to skip OIDC entirely for local development — see "Local development without OIDC" below |
BASE_URL must be an https:// URL (a real hostname, not localhost) in any
deployment you actually log into. The session cookie this app sets after an
OIDC login is marked Secure, so browsers only store and return it over a
secure context — over plain http:// on a non-localhost host the cookie is
silently dropped and login appears to do nothing: you are redirected back to
the home page still logged out, with no error anywhere.
Terminate TLS in front of the container (reverse proxy, ingress, tunnel) and
point BASE_URL at that public HTTPS address. http://localhost:8090 is fine
for local development only, because browsers treat localhost as a secure
context.
For running the app on your own machine with no OIDC provider at all, set
DEV_AUTH=true. This swaps the entire OIDC login flow for two fixed,
pre-seeded PocketBase accounts you log into with one click:
DEV_AUTH=true
BASE_URL=http://localhost:8090
SESSION_SECRET=<any 32+ character string>OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET are not required in this
mode. Boot the app, then visit ${BASE_URL}/dev/login and click "Log in as
regular user" or "Log in as admin" — no credentials to type, no provider to
register with.
This is enforced to be local-only. DEV_AUTH=true requires BASE_URL to
be http://localhost... or http://127.0.0.1... — the app refuses to boot
under DEV_AUTH=true with any other BASE_URL, so this cannot accidentally
end up active in a real deployment. Never set DEV_AUTH=true anywhere other
than your own machine.
The two seeded accounts let you exercise both is_admin code paths (e.g. the
/admin cleanup view, edit/delete permissions) without needing a real OIDC
group claim.
cp .env.example .env
# edit .env with real values
docker compose up -d --buildThe app listens on port 8090. All state (SQLite database + uploaded photos)
lives in the pb_data named volume.
On first boot, PocketBase has no superuser account yet. Create one to access the PocketBase admin panel (used only for initial setup — category management and configuring automated backups — not for day-to-day app use):
docker compose exec device-lending ./device-lending superuser upsert admin@example.com <a-strong-password>Then visit ${BASE_URL}/_/ to log into the PocketBase admin panel, where you
can add device categories (Collections → categories) and configure
S3-compatible automated backups (Settings → Backups).
The app sends notification emails for the whole lending workflow (new request, request declined, handover, device now unavailable, device available again, request withdrawn, device removed). There are no SMTP environment variables: mail is configured in the PocketBase admin panel you just logged into, under Settings → Mail settings — enable the SMTP server and fill in host, port, credentials and the sender address/name. Use its "Send test email" button to confirm the settings before relying on them.
Until SMTP is configured, notification emails simply do not go out: PocketBase
falls back to a local sendmail binary, which is not present in this app's
Alpine-based image, so every send fails. This is logged (visit Logs in the
admin panel) but is deliberately not treated as an error by the app — the
underlying action still succeeds and is saved, so a user who submits a request
or hands a device over gets the normal confirmation and the state change
sticks. Only the notification is lost, so people will not be told about
requests they need to act on until you configure SMTP.
Log into the app once via ${BASE_URL}/oidc/login so your user record
exists, then make sure your account is a member of the OIDC_ADMIN_GROUP
group in your OIDC provider. Admin status syncs on every login.