Skip to content

Authentication

Authentication runs on Better Auth inside the Worker, stored in D1 through the Drizzle adapter. Its endpoints live under /api/auth; the full list is in the Auth REST API reference.

Client How it signs in
Web and PWA Session cookie on gymgym.club. Google, GitHub, passkeys, or a guest account that can be upgraded later by linking a provider.
CLI Device code flow: gymgym login shows a code, you approve it at /device while signed in, and the CLI keeps a bearer token. Only first-party client ids (DEVICE_CLIENTS in packages/api/src/auth/plugins.ts) may start this flow.
Native apps and watches (later) Bearer token from the same device flow or handed over by the phone app.
Telegram Mini App (later) A Better Auth plugin that checks Telegram’s signed initData.

Set these Worker secrets to enable a provider. Without them, the button is hidden.

Provider Secrets Callback URL
Google GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET https://gymgym.club/api/auth/callback/google
GitHub GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET https://gymgym.club/api/auth/callback/github
Cloudflare (optional) ENABLE_CLOUDFLARE_LOGIN=1, CLOUDFLARE_CLIENT_ID, CLOUDFLARE_CLIENT_SECRET https://gymgym.club/api/auth/callback/cloudflare

GitHub allows one callback URL per OAuth app, so staging and local development each need their own app. Google accepts several callback URLs on one client.

Passkeys use relying party id gymgym.club (PASSKEY_RP_ID), so they also work on subdomains such as staging.gymgym.club.

Accounts whose email is in ADMIN_EMAILS, or whose user id is in ADMIN_USER_IDS (both comma-separated), get the admin role when they sign up or sign in, so listing an existing account works on its next sign-in. Taking someone off a list does not remove the role. With INVITE_ONLY=1, new accounts need an invite code created by an admin.

Variable Effect
PASSWORD_LOGIN=1 Staging or production also accept an email and password. Sign-up by password stays off: a signed-in user adds a password in Settings (the setPassword mutation) and changes it with /api/auth/change-password. So nobody can register an address they don’t control, and Google or GitHub linking by email can’t hand such an account to whoever registered the address first. Someone who forgets the password signs in the way they first did and sets a new one.
ALLOW_GUEST=0 Turns off guest accounts. New passkey-only accounts start as guests, so they go too; existing passkeys keep working.
DEFAULT_LANG=de Language for visitors and new accounts whose browser asks for none of the supported ones (English otherwise).

Locally (ENVIRONMENT=development) email and password sign-in and sign-up are always on, for the seed accounts and testing.

In Settings, each passkey can be renamed or removed; the last way into a guest account can’t be removed.

Every new session adds a row to sign_in: when, how (google, github, passkey, password, guest, cli), the country from Cloudflare’s cf-ipcountry and a short device label such as “Firefox · macOS”. IP addresses and full user agents are not stored. Admins read it on the dashboard (admin.signIns); rows older than 90 days are pruned by the hourly cron.

Better Auth’s rate limiter is on in staging and production and off locally, so tests and development aren’t throttled.