- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Feature Flags in Laravel: Ship Safely Without Long-Lived Branches
About Post
The feature branch was supposed to live for a week. It's now six weeks old, and main has moved on without it. The merge conflict list scrolls. Two people are scared to touch it. Someone suggests "just merging it on Friday and seeing what happens."
Long-lived branches feel safe because unfinished work stays out of production. But they move the risk to the merge, which is the worst possible moment to discover it.
Feature flags flip that around. You merge small pieces into main every day, deploy them, and keep them switched off until they're ready. The code ships continuously. The feature launches when you decide.
A feature flag is just an if
That's honestly all it is at its core:
if (Feature::active('new-payment-page')) {
return view('payments.new');
}
return view('payments.legacy');
What makes it powerful is who decides the answer, and when. A good flag system lets you change the answer without deploying, per user, per group, or for a percentage of everyone. Deploy and release become two separate events.
Laravel has an official package for this: Laravel Pennant. It's small, it's first-party, and it does exactly what most apps need.
Setting up Pennant
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
That creates a features table. With the default database driver, Pennant stores the resolved value of each flag for each user, which matters more than it sounds. We'll come back to that.
Define flags in a service provider's boot() method:
use Illuminate\Support\Lottery;
use Laravel\Pennant\Feature;
Feature::define('new-payment-page', fn (User $user) => match (true) {
$user->isStaff() => true,
default => Lottery::odds(1 / 10),
});
Staff always get the new page, so your own team tests it first in production. Everyone else has a one-in-ten chance. Pennant checks against the authenticated user by default, and Feature::for($tenant)->active(...) lets you check a specific one.
Using flags in the right places
Besides the plain if, Pennant gives you a Blade directive for views:
@feature('new-payment-page')
@include('payments.partials.new-summary')
@else
@include('payments.partials.summary')
@endfeature
And middleware for whole routes, which returns a 400 response by default when the flag is off (you can customise it):
use Laravel\Pennant\Middleware\EnsureFeaturesAreActive;
Route::get('/payments/v2', NewPaymentController::class)
->middleware(EnsureFeaturesAreActive::using('new-payment-page'));
Try to check a flag in as few places as possible. One check at the entry point (a route, a controller, a top-level component) is easy to remove later. Twenty checks scattered through the code are a cleanup project.
The kill switch
This is the part that makes Friday deploys boring. If the new page misbehaves, you don't revert and redeploy. You run one line, from Tinker or a small admin screen:
Feature::deactivateForEveryone('new-payment-page');
Everyone is back on the old path in seconds, while you fix the problem calmly. One detail from the docs: this updates the values Pennant has already stored. A user who has never been evaluated still goes through your definition, so follow up by changing the definition too (or have it read a setting you can flip).
For risky integrations, like a new payment gateway or an external API, it's worth adding a permanent "operational" flag for exactly this reason.
The gradual rollout gotcha
Here's what catches people. Because Pennant stores each user's resolved value, changing the odds from 1 in 10 to 1 in 2 does nothing for users who already got false. Their answer is saved.
To re-evaluate, you purge the stored values with php artisan pennant:purge new-payment-page. But with a lottery, purging re-rolls everyone, so some users who had the new page will lose it. That's a confusing experience.
The fix is to make the rollout deterministic. Put each user in a stable bucket from 0 to 99 based on their ID:
Feature::define('new-payment-page', fn (User $user) => match (true) {
$user->isStaff() => true,
default => crc32((string) $user->id) % 100 < 10, // 10%
});
Now when you change 10 to 50 and purge, everyone who had the feature still has it, because their bucket hasn't moved. The rollout only ever grows.
Every flag is a small piece of technical debt. When you create one, decide when it dies: "remove two weeks after 100% rollout". Put it in the ticket. Flags without an end date become permanent branches hiding inside your code.
Testing both paths
A flag means two versions of your app exist at once, and both need to work. In tests, redefine the flag to force each path:
public function test_new_payment_page_is_shown_when_enabled(): void
{
Feature::define('new-payment-page', true);
$this->actingAs(User::factory()->create())
->get('/payments')
->assertViewIs('payments.new');
}
Write the matching test with false. When you remove the flag, the old test goes with it.
Cleaning up
Once a feature is fully rolled out and stable, finish the job:
- Remove the old code path and every check of the flag.
- Remove the definition.
- Run
php artisan pennant:purge new-payment-pageto clear its stored values. - Delete the tests for the old path.
Kinds of flags age differently. Release flags should live for weeks. Experiment flags live until you've decided. Operational kill switches and permission-style flags ("this client has the reports module") can live for years, and that's fine, as long as you know which kind each one is.
In short
- Merge small, deploy daily, release with a flag.
- Check flags at entry points, not everywhere.
- Keep a kill switch for anything risky.
- Use stable buckets for percentage rollouts.
- Give every release flag an expiry date, and keep it.
How many feature flags are hiding in your codebase right now that should have been removed months ago? Be honest. I'll go first: more than zero.

Be first to comment it...