Skip to content

Authentication

GiftWrapt uses better-auth for sessions. Out of the box you get email + password sign-in, with optional OIDC, WebAuthn passkeys, and TOTP-based two-factor.

The default and the only mode that’s enabled with no extra configuration. Anyone with a working email address can sign up; the first account on a fresh deployment is automatically promoted to admin.

Password resets are tokenized URLs sent over email. If outbound email isn’t configured the reset path still appears to work from the user’s perspective (so it doesn’t leak which accounts exist), but the email itself is a no-op. Configure Resend or SMTP before relying on password resets.

Password reset tokens are good for 60 minutes.

Admins can wire up any OpenID Connect provider (Authentik, Authelia, Keycloak, Google Workspace, etc.) from the admin settings UI. The form takes:

  • Issuer URL (preferred) - GiftWrapt discovers the endpoints automatically via /.well-known/openid-configuration.
  • Authorization URL + Token URL (alternative) - explicit endpoints if your provider doesn’t expose discovery.
  • Client ID and Client Secret.
  • Scopes - defaults to openid email profile.

OIDC config is loaded once at server startup. Saving changes in the admin form requires a server restart to take effect, the same as Audiobookshelf and other better-auth-based apps.

If the config is invalid at boot, GiftWrapt logs a warning and falls back to email + password only. Operators can still sign in, fix the config, and restart.

Passkeys are an add-on, not a primary sign-in method. Users sign in with email + password first, then enroll a passkey from their account settings page. Subsequent sign-ins can use the passkey instead.

The relying-party identifier defaults to the hostname of BETTER_AUTH_URL. If you’re testing over an IP address or an ngrok tunnel, set BETTER_AUTH_URL to the exact origin the browser loads so WebAuthn’s same-origin check passes.

Users can enable TOTP-based 2FA from their account settings. Enrollment requires entering a valid code before 2FA is flipped on - so a misconfigured authenticator app can’t lock the user out. Backup codes are generated at enrollment time and shown once; users should store them somewhere safe.

The companion iOS app authenticates via per-device API keys minted from the user’s account settings. Each install gets its own key, stored in the device Keychain. Revoking one device’s key doesn’t sign the user out of the web.

API keys are kept fully separate from the web session cookie. The web’s CSRF defense relies on the sameSite=lax cookie, and API keys never travel as cookies.

Session cookies are httpOnly, sameSite=lax, and Secure (when the deployment is HTTPS). Two environment variables affect cookie behavior:

  • TRUSTED_ORIGINS - comma-separated extra origins to trust beyond BETTER_AUTH_URL. Set this if you reach the same instance from more than one hostname (e.g. a public domain plus a LAN IP).
  • INSECURE_COOKIES=true - drops the Secure flag, so cookies work on plain-HTTP origins. The server refuses to boot with both INSECURE_COOKIES=true and an HTTPS BETTER_AUTH_URL.

better-auth’s built-in rate limiter is enabled with database-backed counters (so the limits hold across instances on Vercel / Render / Railway). Sign-in, sign-up, password change, and session reads all have stricter caps applied automatically.

API keys have their own per-key limit of 300 requests per minute. That’s tuned for a chatty native client that pings verifyApiKey + getSession on every request; it still throttles a runaway client or a leaked key.