Rush CMS: a multi-tenant headless CMS serving entire agencies from a single platform
Proprietary SaaS
Developer tools, headless CMS for agencies and multi-site operations
Architecture, development, infrastructure, and operations
Continuous development and production since 2025
Rush CMS is my SaaS: a multi-tenant headless CMS built on Laravel 12 and Filament 4 for people who manage many sites at once — agencies and operations with dozens of domains under one roof. Instead of maintaining 50 separate WordPress installations, a single Laravel instance serves every client with perfect data isolation, each with its own content, users, permissions, and API.
Unlike the rest of my portfolio, this is not a client project. It's a product I architected, built, and maintain alone, end to end: from the data model to the infrastructure, from the admin panel to the API layer, from tenant isolation to the 603-test pipeline. It's the piece where my engineering decisions show up with no intermediary, because every one of them was mine.
And it's genuinely in production. The very site you're reading this on, rafhael.pro, runs on Rush CMS. So does a bakery with a complete digital menu. Radically different use cases served by the same codebase — which is exactly the point of a well-built multi-tenant CMS. The product lives at rushcms.com.
The problem Rush solves
Anyone maintaining many small-to-medium sites knows the pain: each site becomes a separate WordPress instance (or similar), with its own updates, its own backups, its own attack surface, its own hosting account. Ten sites become ten systems to maintain. Fifty become an operational nightmare.
The obvious alternative, "one WordPress multisite," solves part of the problem and creates others: coupling between sites, plugins that break everyone at once, and a content model locked to "posts and pages" that doesn't fit anyone who needs their own data structures.
I wanted something else: a platform where adding a new site means adding a tenant, not standing up a server. Where the content model is defined by the person using it, not imposed by the tool. And where delivery performance doesn't depend on cache improvised on top of a system that's slow by nature.
The architectural decisions
This is the heart of the case, because Rush CMS is, above all, a set of deliberate technical decisions. Each one has a reason.
Multi-tenancy with real isolation via global scopes
Tenants are 100% isolated, and there is no super-admin that sees every client's data — a deliberate security and privacy decision. Isolation is enforced by an Eloquent global scope: every query on a tenant model is automatically filtered by site_id from the tenant resolved by Filament. The result is zero data leakage between sites, with the isolation rule explicit in the code and covered by tests, not hidden behind framework magic.
In a multi-tenant product this isn't a detail: a bug that leaks one client's data into another's isn't an ordinary bug, it's a security incident. That's why isolation is tested, not trusted.
Dynamic content modeling in PostgreSQL with JSONB
This is the project's most important decision. Instead of creating a table and a migration for each new content type, Rush stores Collection schemas and Entry data in JSONB on PostgreSQL, with GIN indexes for querying. In practice: a user can create a new Collection, with its own fields, without a single migration running on the database.
This is what enables the Notion/Statamic-style Collections: lists and databases the user builds themselves, which can even be chained as hierarchical dependencies. Car brands that unfold into models. States that unfold into cities, in a reactive cascade. Or something as simple as a list of "technology types" applied to a collection to become a select field when creating an entry. The data model stops being defined by me, in code, and becomes defined by the user, at runtime.
Runtime PHP class generation for Custom Blocks
This is the technical trick I'm proudest of. The content editor's Custom Blocks are defined by the user in the database — name, icon, fields — and need to become real PHP classes for the editor (Filament RichEditor/TipTap) to recognize them.
The first approach was class_alias(), but it has a limitation: inside the method, static::class still returns the base class, so the block doesn't know what it is. The solution was to generate real subclasses at runtime via eval(), where each block knows its own slug.
Using eval() is the kind of decision that demands justifying the security trade-off, and I justify it: the code template is hardcoded, the only dynamic part is the slug (validated by the database and sanitized via Str::studly()), and no user input ever reaches eval(). It's the right solution to a real problem, with the risk mapped and contained — not gratuitous cleverness.
Delivery performance: Octane + RoadRunner, sub-20ms responses
A headless CMS lives or dies by API latency, because that's what feeds the frontends. I run Rush on Laravel Octane with RoadRunner (v2025.1.6, 4 workers, 256MB each), keeping the application alive in memory between requests instead of booting and tearing down the framework on every call. The result is API responses of ~20ms on simple endpoints and ~50ms on complex ones, fast enough to serve static sites via SSG with rebuild webhooks, with Redis API caching on top.
Running Octane imposes discipline: shared state between requests is a trap. The code was written with that in mind — no static properties accumulating request data, dependency injection instead of stateful singletons, and workers that recycle every 10,000 jobs to contain memory leaks.
i18n as a foundation, not a patch
Rush is i18n-first: built for multiple languages from the ground up, with Spatie Translatable. Entries and other content can be created in multiple languages natively, and the platform itself is available in Portuguese and English. Internationalization bolted on later is always a scar; here it's structure.
Choosing PostgreSQL: a decision backed by data
Rush started on MySQL and migrated to PostgreSQL — and that migration was an architectural decision, not a cosmetic one. Since the whole system depends on JSONB for the dynamic content model, I measured the gain: JSON queries became 3 to 5 times faster with GIN indexes, I gained native full-text search with Portuguese stemming, and better concurrency for the queue workers. Migrating was swimming with the current of what the product already was.
What Rush does
Underneath those decisions, Rush is a complete CMS, not a prototype:
- Custom Collections with 13+ field types and a reusable list/database system chainable as dependencies
- Custom Blocks generated at runtime, with a Notion-style content editor (TipTap) and ready-made blocks like YouTube and a lightbox gallery
- Homepage block builder to assemble the front page visually
- Form builder with automatic UTM/referrer capture and email dispatch to both company and sender
- Hierarchical menu builder, tags, categories, and webhooks
- Membership system for sites with gated, members-only content
- User and role management with 104+ permission nodes, in Filament's
action:resourceconvention - Google Analytics integration via OAuth2 plus a built-in pageview analytics system
- WordPress importer that converts HTML into blocks, with preview and dry-run
- MCP integration: control the CMS via LLM (ChatGPT, Claude) in natural language, with per-tenant isolated tokens
- Automatic WebP conversion on every image field, with 50-70% storage savings
- A fully bilingual (PT-BR/EN) platform
The engineering behind it
What makes Rush sustainable over the long term isn't the feature list, it's the rigor underneath it:
- 603 automated tests with Pest, because in a multi-tenant product, testing isolation isn't optional.
- PHPStan/Larastan Level 5 for static analysis and catching errors before runtime, with Laravel Pint enforcing code style.
- CI/CD via GitHub Actions running tests, static analysis, code style, security audit, and frontend build on every push, deploying in ~25 seconds.
- Systematic N+1 elimination, eager loading, and Redis caching — the invisible work that keeps a system fast as it grows.
- Documentation as code, ADR-style (Architecture Decision Records). Every significant decision is recorded, because a one-developer product only survives the passage of time if decisions don't live solely in the head of whoever made them.
The infrastructure
Rush runs on Hetzner servers managed via Coolify, with PostgreSQL 16, Redis 7, Nginx, and Supervisor for the queue workers. It's a self-hosting choice that keeps costs predictable — the whole operation runs in the low tens of reais per month — and control in my hands, without depending on expensive managed platforms. Sentry error tracking, automated S3 backups with a lifecycle policy, and monitoring round out the picture. The same principle that guides the code guides the infrastructure: the simplest, most direct solution that delivers the result, with no complexity bought for status.
What this project demonstrates
Rush CMS is the walking proof of concept of how I work. It shows three things a client case rarely shows with the same clarity:
Architectural depth. The hard decisions — tenant isolation via global scope, dynamic JSONB modeling, runtime class generation, performance via Octane, the measured migration to PostgreSQL — were all made and defended by me, and they're in production holding real data for real clients.
Long-term maintenance. Building an MVP is easy; keeping a multi-tenant product alive, tested, and fast over months, with CI/CD and decision documentation, is what separates those who prototype from those who sustain systems in production.
Pragmatism under constraint. A single developer, cheap infrastructure, a lean stack, and still a CMS architected for 50+ isolated sites, served under 20ms. It's not about having infinite resources; it's about making the right call at every fork.
If NR Secure shows that I solve a client's pain, Rush CMS shows that I build and sustain the entire product.