Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Symfony Bundle Laravel Package

betterauth/symfony-bundle

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, so as a Laravel developer your first job is a mental translation, not a code translation. Map the concepts before touching config:

Laravel reflex BetterAuth equivalent
Sanctum / Passport PASETO v4.local access tokens + rotating refresh tokens
Fortify / Breeze actions Generated auth controllers (CredentialsController, 2FA, magic-link)
config/auth.php guards Symfony firewall + bundle mode (api / session / hybrid)
php artisan migrate + published models Doctrine entities with app-owned persistence + better-auth:user-fields
php artisan vendor:publish better-auth:publish-templates, better-auth:setup-features
Laravel scheduler cleanup better-auth:cleanup:sessions, better-auth:cleanup:tokens

Minimum steps

composer require betterauth/symfony-bundle:0.0.20   # pin exactly, pre-1.0
php bin/console better-auth:install
php bin/console better-auth:setup-features --preset=full --with-controllers --migrate
php bin/console debug:router | grep auth
php bin/console debug:config better_auth

Hard prerequisites before you start: PHP 8.4, Symfony 6.4/7/8, Doctrine ORM 3, MigrationsBundle 4. If your host or CI is on 8.2/8.3, stop here — this is not a "try it and see" package.

Where to look first, in this order:

  1. docs/01-INSTALLATION.md, docs/02-CONFIGURATION.md — the config tree is the source of truth.
  2. docs/03-API-MODE.md vs 04-SESSION-MODE.md vs 05-HYBRID-MODE.md — pick your mode before generating controllers; better-auth:switch-mode exists but retrofitting is cheaper than migrating.
  3. docs/11-SECURITY.md — the production hardening checklist, plus changelog/0.0.20.md, which is the release you should actually read.

First use case: an API-first / API Platform 4 backend for a SPA or mobile client. Run --preset=full in api mode: JSON login → PASETO access token in the Authorization header, refresh token in an httpOnly cookie, plus /auth/refresh, /auth/logout. Skip session mode until you have a server-rendered Symfony app.

Implementation Patterns

Pin the version and treat the generated controllers as your code. --with-controllers writes controllers into your src/, not the vendor tree; you own them from that moment. Diff them on every upgrade instead of blindly regenerating — that's also your extension point: add throttling headers, audit logging, or tenant resolution in the generated controller or by decorating the service it delegates to.

Mode selection drives everything downstream:

  • api — stateless PASETO, refresh cookie, best fit for mobile/SPA. refresh_token is a real rotatable credential.
  • session — Symfony session with a hashed token at rest; in 0.0.20 the response deliberately returns refresh_token: null. Do not wire client-side rotation logic that assumes a refresh token here.
  • hybrid — both; useful when a SPA and a server-rendered admin share one backend. Costs the most config surface.

Refresh rotation, correctly. Store the refresh token in an httpOnly cookie (cookie_secure/cookie_http_only default true, cookie_same_site lax). On 401, call refresh once per request via a queued interceptor, then retry. Rotation means a replayed old token should be treated as theft — surface replaced_by invalidation as a forced logout, not a silent retry loop.

2FA enrollment as a two-step action. Generate secret → return otpauth URI for QR → confirm code → enable. login() returns a challenge; login2fa() completes it. Secrets are AES-256-GCM encrypted at rest with a key derived from %better_auth.secret%, so enrollment and verification must hit instances sharing the same secret.

Passwordless + OAuth. Magic-link and email-verification endpoints validate callbackUrl against your trusted frontend origin and return 400 on a foreign host — so configure that origin explicitly per environment (https://app.example.com in prod, http://localhost:3000 in dev). For OAuth, verify the account-linking path handles Google's verified_email field (fixed in 0.0.10); linking an existing email is the highest-risk flow, so test it manually.

Application-owned persistence. Start with the generated entities, then extend via better-auth:user-fields rather than editing vendor mapping. If you need custom storage, implement the bundle's repository interfaces — the Doctrine session and TOTP repositories are swappable services, which is also how you'd point at a read/write split or a cache-backed session store.

Operationalize the cleanup commands in Symfony Scheduler (or cron):

$schedule->add(RecurringMessage::every('1 hour', new RunCommand('better-auth:cleanup:sessions')));
$schedule->add(RecurringMessage::every('1 day', new RunCommand('better-auth:cleanup:tokens')));

Testing. Use SQLite for functional tests (sqlite:///%kernel.project_dir%/var/test.db) — it's covered in CI. Reset the rate limiter between tests (use array cache/lock in test env) or your login tests will hit the new 5-attempts/15-minutes limit and give you confusing 429s. Assert on the hashed-at-rest invariants: session tokens are stored as sha256(token), TOTP seeds carry an enc:v1: prefix.

Gotchas and Tips

Pre-1.0 with 4 stars and zero dependents. Every release so far is 0.0.x. Pin exact versions (:0.0.20) in composer.json, never ^0.0, and read changelog/ before bumping. Budget for self-support: fork access and the ability to patch a controller is realistic; waiting on upstream is not.

PHP 8.4 is a hard floor — newer than most Laravel shops run. Check CI images, Docker base, and any shared-hosting target before committing.

Secret handling is the #1 production footgun. BETTER_AUTH_SECRET must be long, stable, and identical across every instance. Rotating it makes all encrypted TOTP secrets unreadable, permanently locking out 2FA users. Also note that secret-length validation happens at runtime in TokenService, not at container compile time (the compile-time check was removed in 0.0.13 because Symfony's ValidateEnvPlaceholdersPass substitutes short placeholders for %env(...)%). Consequence: the app boots fine with a bad secret and fails on the first login. Add your own boot-time smoke test hitting a token endpoint in staging.

Undeclared config keys silently vanish. 0.0.20 shipped cookie_secure/cookie_http_only/cookie_same_site in the recipe but they weren't in the configuration tree, so they could be dropped without error. Always verify with php bin/console debug:config better_auth after editing YAML. In local dev over plain HTTP, cookie_secure: true means the refresh cookie is never set — flip it per-env, don't disable it globally.

Rate limiting is now in front of the password and TOTP checks (5 / 15 min per IP+email). Two implications: the limiter needs a real shared store (Redis/DB) behind multiple app servers, and behind a proxy you must configure trusted proxies or every request looks like the load balancer's IP and you'll globally lock out logins.

Don't enable QueryParameterTokenExtractor. It's intentionally excluded from the default ChainTokenExtractor — tokens in URLs leak into access logs, Referer headers, and browser history. If a third party asks for ?token=, make them use the header.

Legacy data migrates itself, on first access. After upgrading to 0.0.20, plaintext session rows are rehashed and plaintext TOTP secrets re-encrypted in place. This is by design (no composer hook, no migration step), but it means: mixed-version deploys during rollout will behave inconsistently, and if you rotate the secret before upgrading, the re-encryption writes garbage. Upgrade code first, confirm, then touch secrets — never the reverse.

The better-auth:* command namespace is inconsistent with Symfony's usual app:/vendor-prefixed convention; debug:router | grep auth is more reliable than guessing route names. Same for controllers: read the generated routes rather than assuming /login vs /auth/login.

Watch the dependency floor. Doctrine ORM ^3 and MigrationsBundle ^4 will collide with older bundles in a legacy app. Check composer why-not doctrine/orm ^3 before you promise a migration timeline — this bundle is a modernization forcing function, not a drop-in for an app still on ORM 2.

Logging hygiene is handled for you — emails are masked (ke***@domain) in failure logs. Keep it that way: don't log request bodies around these endpoints or you'll undo the effort. better-auth:setup:logging wires the channel; use it rather than rolling your own monolog handler.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
encore2/laravel-admin2
cboxdk/laravel-console-kit
shapecode/doctrine-dbal-datetimeutc
gilads-otiannoh254/inertia-protocol
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
adachsoft/cli-process-tester
adachsoft/changelog-linter
adachsoft/collection
adachsoft/php-code-style
adachsoft/api-version-bundle
cgv/localized-resource