A modular PHP framework for multi-site, multi-distributor application development.
Razy lets you manage multiple websites, APIs, and services from a single codebase. Each distributor runs its own set of versioned modules with independent routing, templates, and database access — while sharing common services through a unified module system.
- Key Features
- Requirements
- Installation
- Quick Start
- Docker
- Architecture Overview
- Module Lifecycle
- Core Concepts
- Package Management (Composer Integration)
- Standalone Package System
- Demo Modules
- Performance: Razy vs Laravel
- Testing
- Documentation
- Roadmap
- Version Milestone Summary
- Contributing
- Development Journey
- License
| Category | Highlights |
|---|---|
| Multi-Site Architecture | Run multiple distributors (sites/apps) from one installation with independent module sets, routing, and configuration |
| Module System | Dependency-aware loading with 14 lifecycle hooks, cross-module APIs, event emitters, and bridge commands |
| Template Engine | Block-based rendering with modifiers, function tags, WRAPPER/INCLUDE/TEMPLATE/USE/RECURSION blocks |
| Database Layer | Multi-driver (MySQL, PostgreSQL, SQLite) with fluent query builder, Simple Syntax, ORM, migrations, and schema management |
| CLI Tooling | 20+ commands — build, pack, publish, install from GitHub, interactive shell (runapp), bridge calls |
| Thread System | Process-based concurrency via ThreadManager with spawn, await, and joinAll |
| Package Management | Version-locked modules, Composer integration, phar distribution, repository publishing |
| Standalone Packages | Run modules as CLI apps (.phar), exec/serve modes, dependency orchestration, inter-package API & events |
| Built-in Classes | SSE, XHR (CORS), Mailer (SMTP), DOM builder, Crypt (AES-256), Collection, HashMap, YAML, OAuth2, Cache (PSR-16), Authenticator (TOTP/HOTP 2FA), FTPClient, SFTPClient, WebSocket |
- PHP 8.2 or higher
- Extensions:
ext-zip,ext-curl,ext-json - Composer (recommended)
composer require rayfunghk/razydocker pull ghcr.io/rayfunghk/razy:latest
docker run -p 8080:8080 ghcr.io/rayfunghk/razygit clone https://github.com/RayFungHK/Razy.git
cd Razy
composer install
php build.phpIf the application is not at the web root (for example https://localhost/abc/ with files under …/htdocs/abc/), Razy builds RAZY_URL_ROOT and module asset URLs from RELATIVE_ROOT. The framework prefers the path of SYSTEM_ROOT relative to DOCUMENT_ROOT, and falls back to dirname($_SERVER['SCRIPT_NAME']) when those paths do not align (symlinks, mounts, or unusual server variables). Ensure your front controller (index.php) is invoked with a correct SCRIPT_NAME (typical Apache, nginx + PHP-FPM, and Caddy setups do). Rewrite rules should still map /your-prefix/webassets/… to the framework as documented.
# Build the Razy environment
php Razy.phar build
# Create a distributor
php Razy.phar init dist mysite
# Generate rewrite rules
php Razy.phar rewrite mysiteThis creates the working directory structure:
project/
├── Razy.phar # Framework binary
├── config.inc.php # Global configuration
├── sites.inc.php # Domain → distributor mapping
├── index.php # Web entry point
├── shared/module/ # Cross-distributor modules
└── sites/mysite/ # Your distributor
├── dist.php # Distributor config (tags, modules, bridge)
└── vendor/module/ # Your modules
├── module.php # Module metadata
└── default/
├── package.php # Package config (API name, requires)
└── controller/ # Route handlers
Map domains to distributors in sites.inc.php:
return [
'domains' => [
'example.com' => [
'/' => 'mysite@v2', // standard mapping with tag
],
],
];Reuse the same distributor across domains with different config folders:
// sites/mysite/dist.php
return [
'dist' => 'mysite',
'modules' => [ /* ... */ ],
'config_mapping' => [
'localhost' => 'local', // loads sites/mysite:local/dist.php
'example.com@v2' => 'prod', // loads sites/mysite:prod/dist.php
'staging.com' => '!/var/www/cfg', // absolute path override
],
];Razy ships with a complete Docker setup for development and testing.
# Start PHP dev server + Caddy reverse proxy
docker compose -f .docker/docker-compose.yml up
# With live reload and auto-build
docker compose -f .docker/docker-compose.yml -f .docker/docker-compose.dev.yml upRun all tests including those that require Linux-only extensions:
docker compose -f .docker/docker-compose.test.yml up --build --abort-on-container-exit
# → 4,564 tests, 8,178 assertions, 0 skippedOpen the project in VS Code and select "Reopen in Container" — the DevContainer is pre-configured with PHP 8.3, Composer, and all required extensions.
┌─────────────────────────────────────┐
│ Application │
│ (Domain matching → Distributor) │
└──────────────┬──────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Distributor │ │ Distributor │ │ Standalone │
│ (mysite) │ │ (admin) │ │ (lite) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Modules │ │ Modules │ │ Modules │
│ (tagged)│ │ (tagged)│ │ (direct)│
└─────────┘ └─────────┘ └─────────┘
Each distributor maps to a domain/path via sites.inc.php and loads its own versioned module set. Modules communicate through APIs, events, and bridge commands. The Standalone mode provides a lightweight runtime for single-module applications.
Modules progress through a well-defined lifecycle during each request:
__onInit → __onLoad → __onRequire → (await callbacks)
→ __onReady → __onScriptReady / __onRouted → __onEntry
return new class extends Controller {
public function __onInit(Agent $agent): bool
{
// Register routes, APIs, events, scripts
$agent->addLazyRoute(['dashboard' => 'dashboard']);
$agent->addAPICommand('getUser', 'api/get_user.php');
$agent->listen('auth/user:onLogin', 'onUserLogin');
return true;
}
public function __onReady(): bool
{
// Safe to call APIs here — all modules are loaded
return true;
}
};Two routing strategies: lazy routes (convention-based) and regex routes (pattern-based).
// Lazy: /modulecode/users/list → controller/users/list.php
$agent->addLazyRoute(['users' => ['list' => 'list']]);
// Regex: /api/user-42/profile → controller/Route.user_profile.php
$agent->addRoute('/api/user-(:d)/profile', 'user_profile');Modules expose and consume APIs for inter-module communication:
// Provider: register in __onInit
$agent->addAPICommand('getData', 'api/get_data.php');
// Consumer: call from any handler
$result = $this->api('vendor/provider')->getData($id);Block-based templates with variables, modifiers, conditionals, and iteration:
{$user.name|capitalize}
{@if $user.role="admin"}
<span class="badge">Admin</span>
{/if}
{@each source=$items as="item"}
<li>{$item.name} — {$item.price}</li>
{/each}
Shorthand syntax for joins, WHERE clauses, and JSON operations:
// Simple syntax generates complex SQL automatically
$stmt = $db->prepare()
->from('u.user-g.group[group_id]')
->where('u.user_id=?,!g.auths~=?')
->assign(['auths' => 'view', 'user_id' => 1]);
// → SELECT * FROM `user` AS `u` JOIN `group` AS `g`
// ON u.group_id = g.group_id
// WHERE `u`.`user_id` = 1
// AND !(JSON_CONTAINS(JSON_EXTRACT(`g`.`auths`, '$.*'), '"view"') = 1)php Razy.phar build # Build environment
php Razy.phar runapp mysite # Interactive shell
php Razy.phar install owner/repo # Install from GitHub
php Razy.phar pack distCode # Package modules
php Razy.phar publish # Publish to repository
php Razy.phar validate distCode # Validate & install deps
php Razy.phar bridge '{"dist":"..."}' # Cross-distributor callRazy includes a built-in Composer-compatible package manager that downloads, extracts, and version-locks third-party packages from Packagist or any private mirror — scoped per distributor so each site gets its own isolated dependency tree.
-
Modules declare prerequisites in their
package.phpusingvendor/packagenotation:// vendor/blog/default/package.php return [ 'module_code' => 'vendor/blog', 'version' => '1.0.0', 'api_name' => 'blog', 'require' => ['vendor/auth' => '>=1.0.0'], // Razy module dependency 'prerequisite' => [ // Composer package dependency 'monolog/monolog' => '^3.0', 'guzzlehttp/guzzle' => '^7.0', ], ];
-
On compose, the framework collects all
prerequisiteentries across every loaded module, resolves version constraints, and downloads matching packages:php Razy.phar compose mysite # → Fetches metadata from Packagist # → Downloads & extracts monolog/monolog ^3.0 # → Downloads & extracts guzzlehttp/guzzle ^7.0 # → Writes autoload/lock.json
-
Per-distributor isolation — packages are extracted into
autoload/{distributor_code}/with PSR-4/PSR-0 namespace mapping, and a singleautoload/lock.jsontracks installed versions keyed by distributor:autoload/ ├── lock.json # Version lock (all distributors) ├── mysite/ # Packages for "mysite" distributor │ ├── Monolog\ │ └── GuzzleHttp\ └── admin/ # Packages for "admin" distributor └── Monolog\
The package manager is transport-agnostic. By default it fetches from Packagist over HTTPS, but you can point it at any mirror using a pluggable transport:
| Transport | Protocol | Use Case |
|---|---|---|
HttpTransport |
HTTP/HTTPS | Packagist, Satis, Private Packagist, GitHub |
FtpTransport |
FTP/FTPS | FTP mirrors with optional TLS |
SftpTransport |
SFTP | SSH-based secure transfer |
SmbTransport |
SMB/CIFS | Windows network shares, Samba |
LocalTransport |
File system | Local directory or mounted drive |
All transports implement PackageTransportInterface. Set a global default at bootstrap:
use Razy\PackageManager;
use Razy\PackageManager\FtpTransport;
PackageManager::setDefaultTransport(new FtpTransport(
host: 'mirror.internal',
username: 'deploy',
password: 'secret',
basePath: '/composer',
));Supports the same constraint syntax as Composer: ^1.0, ~2.3, >=1.2.0, *, exact versions, and stability flags (@dev, @beta, @RC). Sub-dependencies declared in each package's own require block are resolved recursively.
Full details: Packaging & Distribution wiki
Razy modules can run as standalone CLI applications — packaged as .phar archives and executed outside the web request lifecycle. This turns any module into a self-contained tool, background service, or migration script while retaining full access to Razy's module system, APIs, and events.
┌──────────────────────────────────────────────────────┐
│ php Razy.phar pkg vendor/app │
└──────────────────┬───────────────────────────────────┘
│
┌─────────▼──────────┐
│ PackageRunner │ Orchestrates lifecycle
└─────────┬──────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Prerequisites Dependencies Execution
(Composer) (on_depend) (Standalone or Distributor)
│ │ │
│ ┌────┴─────┐ ▼
│ │ complete │ ┌──────────────┐
│ │ healthchk │ │ Module + │
│ │ load │ │ PackageTrait │
│ └──────────┘ └───────┬──────┘
│ │
└─────────────────────────────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
__onPackage __onPackage __onPackage
Start() Exec() Stop()
Two execution modes:
| Mode | Behaviour | Hook |
|---|---|---|
| exec | Run-to-completion — exits with a code | __onPackageExec() |
| serve | Long-running (HTTP, WebSocket, queue) — blocks until signal | __onPackageServe() |
Two runtime modes:
| Runtime | Flag | How modules load |
|---|---|---|
| Standalone | (default) | Single module + co-modules from on_depend "load" |
| Distributor | -d dist/module |
Full Distributor loads ALL dist modules; target module executes lifecycle |
Every package has a manifest at the archive root:
{
"package_name": "my-api",
"version": "1.0.0",
"description": "My standalone package",
"mode": "exec",
"strict": false,
"on_depend": [
{"package": "db-setup", "wait": "complete"},
{"package": "cache-service", "wait": "healthcheck"},
{"package": "shared-lib", "wait": "load"}
],
"healthcheck": {
"url": "http://localhost:8080/health",
"interval": 2,
"timeout": 30,
"start_period": 5
},
"prerequisite": {
"monolog/monolog": "^3.0"
}
}| Field | Purpose |
|---|---|
package_name |
Unique identifier (e.g., my-api, or vendor/name for dist mode) |
version |
SemVer version string |
mode |
exec (run-to-completion) or serve (long-running) |
strict |
When true, serve mode binds to localhost only |
on_depend |
Dependency orchestration — see below |
healthcheck |
HTTP polling config for serve-mode packages |
prerequisite |
Composer packages to auto-install |
Packages can depend on other packages with three wait strategies:
| Wait Mode | Behaviour |
|---|---|
"complete" |
Run the dependency as exec-mode, block until it exits successfully |
"healthcheck" |
Spawn the dependency as serve-mode, poll its healthcheck URL until healthy |
"load" |
Extract the dependency and inject it as a co-module into the same Standalone runtime — full API, events, and cross-module access |
Add PackageTrait to any Controller to make it package-aware. All hooks use the reserved __onPackage* prefix — no conflict with module closures or routing methods.
class MyController extends Controller
{
use PackageTrait;
public function __onPackageStart(array $packageInfo): bool
{
// Register package API for co-modules to call
$this->registerPackageAPI('greet', fn(string $name) => "Hello, {$name}!");
// Subscribe to package events
$this->onPackageEvent('data:ready', fn(array $data) => $this->processData($data));
return true; // false aborts execution
}
public function __onPackageExec(array $packageInfo): int
{
// Core logic — return value is the process exit code
$this->emitPackageEvent('data:ready', ['key' => 'value']);
return 0;
}
public function __onPackageServe(array $packageInfo): void
{
// Long-running — start HTTP server, event loop, etc.
// This method should BLOCK until shutdown.
}
public function __onPackageStop(): void
{
// Cleanup: close connections, flush buffers
}
public function __onPackageHealthcheck(): bool
{
return true; // healthy
}
}| Hook | When | Return |
|---|---|---|
__onPackageStart |
After prerequisites + dependencies resolve | false aborts execution |
__onPackageExec |
Exec-mode entry point | int exit code (0 = success) |
__onPackageServe |
Serve-mode entry point (blocks) | void |
__onPackageStop |
On shutdown or stop signal | void |
__onPackageHealthcheck |
Polled by dependents or /_razy/health |
bool |
Separate from the Module API/Event system — these are inter-package communication channels for packages running in the same process (e.g., co-modules loaded via on_depend "load").
// Package A: register an API action
$this->registerPackageAPI('transform', fn($input) => strtoupper($input));
// Package B: call it
$result = $this->callPackageAPI('vendor/a', 'transform', 'hello'); // "HELLO"
// Events: pub/sub between packages
$this->onPackageEvent('config:changed', fn($data) => $this->reload($data));
$this->emitPackageEvent('config:changed', ['key' => 'timeout']);# Run an exec-mode package
php Razy.phar pkg migrate -- --fresh
# Run a serve-mode package in background
php Razy.phar pkg my-api --daemon
# Run via Distributor (full module ecosystem)
php Razy.phar pkg -d mysite/vendor/worker -- --queue=emails
# List installed packages
php Razy.phar pkg list
# Show package details
php Razy.phar pkg info my-api
# Stop a running daemon
php Razy.phar pkg stop my-apiA single Controller can serve both web requests and package execution:
public function __onInit(Agent $agent): bool
{
if (defined('RAZY_PACKAGE_MODE')) {
// Running as a standalone package
return true;
}
// Normal web mode — register routes, APIs, etc.
$agent->addLazyRoute(['dashboard' => 'dashboard']);
return true;
}The demo_modules/ directory contains 22 production-ready reference modules organized by category:
| Category | Modules |
|---|---|
| core/ | event_demo, event_receiver, route_demo, template_demo, thread_demo, bridge_provider |
| data/ | collection_demo, database_demo, hashmap_demo, yaml_demo |
| demo/ | demo_index, hello_world, markdown_consumer |
| io/ | api_demo, api_provider, bridge_demo, dom_demo, mailer_demo, message_demo, sse_demo, xhr_demo |
| system/ | advanced_features, helper_module, markdown_service, plugin_demo, profiler_demo |
Each module includes inline documentation and can be copied directly into your distributor's module directory. See the demo README for detailed descriptions.
All items for v1.0 are complete. The framework is in beta — APIs are stable but may receive minor refinements before the final release.
| Status | Feature |
|---|---|
| ✅ | Multi-site distributor architecture with domain routing |
| ✅ | Module system with dependency resolution & 14 lifecycle hooks |
| ✅ | Template engine with blocks, modifiers, conditionals, iteration |
| ✅ | Multi-driver database layer (MySQL, PostgreSQL, SQLite) |
| ✅ | GitHub module installer via CLI |
| ✅ | Thread system (ThreadManager) |
| ✅ | Cross-distributor bridge system |
| ✅ | Module repository & publishing system |
| ✅ | Cache system (PSR-16 SimpleCache with File, Redis, Null adapters) |
| ✅ | Authenticator (TOTP/HOTP 2FA) |
| ✅ | FTP/SFTP file transfer clients |
| ✅ | Database migration system |
| ✅ | Queue / job dispatching |
| ✅ | Rate limiting middleware |
| ✅ | WebSocket server & client |
| ✅ | Docker image & CI/CD pipeline |
| ✅ | Standalone Package System (exec/serve, dependency orchestration, Package API/Events) |
| ✅ | Comprehensive test suite (4,564 tests, 8,178 assertions) |
The foundation release. Razy shipped as an open-source project with full governance (MIT license, contributing guide, security policy), Docker infrastructure, a Composer-compatible package manager, and a comprehensive test suite of 4,564 tests with zero skips. This milestone established the framework's public contract — stable APIs, reproducible builds, and CI/CD from day one.
Two problems drove this release:
1. Performance under persistent workers.
The original FrankenPHP worker loop rebuilt the entire object graph (Application, Container, Standalone, Module, RouteDispatcher) on every single request — the same work a traditional CGI process does, but inside a persistent worker where it should only happen once. Fixing this was straightforward in concept (boot once, dispatch many) but required rethinking how state flows through the framework. The result was a 37× throughput improvement (171 → 6,311 RPS) and 5× faster than Laravel Octane (Swoole) on read-heavy workloads, with an additional round of 8 hot-path micro-optimizations shaving another 4.6% off tail latency.
2. Cross-vendor module identity collisions.
Razy's module system uses a two-part vendor/package code (e.g., acme/logger), but several internal paths — config files, asset URLs, API registration, closure prefixes, rewrite rules — were keyed on only the short class name or alias (the last segment). In a single-vendor setup this worked fine. But in multi-tenant and multi-vendor deployments — the exact use case Razy was designed for — two modules from different vendors with the same package name (e.g., acme/logger and beta/logger) would silently collide: one module could read another's config, hijack its API, shadow its assets, or cause rewrite rules to be silently dropped.
This is not an edge case. In enterprise SaaS environments, module vendors operate independently and cannot coordinate naming. A platform hosting modules from multiple vendors must guarantee vendor-scoped isolation by default. Five collision vectors were identified and fixed, API registration now throws on duplicates instead of silently overwriting, and all identity keys now use the full vendor/package module code.
| Version | Key Changes |
|---|---|
| v1.0-beta | Open-source readiness, Docker, Composer package management, 4,564 tests |
| v1.0.1-beta | 37× worker throughput, 5× vs Laravel, cross-vendor module isolation (5 collision fixes), DI security hardening, pre-commit hook, 4,794 tests |
Full per-version changelogs:
changelog/directory.
Razy was designed from the start as a multi-site, multi-distributor framework: one codebase, many projects. But as the architecture matured — especially with FrankenPHP worker mode keeping the entire Application graph alive in memory — a deeper problem surfaced.
The problem: shared-process trust boundaries.
In a traditional CGI model, each request starts a fresh PHP process. Isolation is free — one request can't reach into another's memory. But in a persistent worker, all distributors and modules share the same process. A malicious or buggy module in one distributor can theoretically access another distributor's data, configs, or API registrations. The v1.0.1-beta cross-vendor collision fixes addressed the naming side of this problem, but the runtime isolation side remains.
The real-world scenario is straightforward: a SaaS platform hosts multiple tenants (clients), each with their own distributors, modules, domains, and data. Today, they all run inside the same PHP process, share the same filesystem, and trust each other implicitly. For internal tooling this is acceptable. For enterprise multi-tenant SaaS — where tenants are separate legal entities with separate data obligations — it is not.
The solution: 1 Tenant = 1 Razy Application environment.
Each tenant runs as a complete, isolated Razy instance — its own container, its own filesystem, its own open_basedir. The local host becomes just another tenant (the "Host Tenant"). Cross-tenant communication uses an explicit HTTP bridge with HMAC authentication, not shared memory. Module code requires zero changes — isolation is enforced at the framework and OS layers.
| Phase | Version | What It Delivers |
|---|---|---|
| Phase 0 — Foundation | v1.0.1-beta ✅ | DI security blocklist, worker dispatch guards, boot-once, distributor caching, module change detection |
| Phase 1 — Tenant Isolation Core | v1.1.0-beta | Bootstrap tenant constants, data path isolation guards, in-memory hotplug (plugTenant/unplugTenant), worker signal integration |
| Phase 2 — Docker Multi-Tenant | v1.1.0 | Hardened tenant Dockerfile (open_basedir + disable_functions), Compose templates, per-tenant config generator |
| Phase 3 — Communication Layers | v1.2.0 | TenantEmitter (HTTP bridge + HMAC), DataRequest/DataResponse (file I/O), __onTenantCall permission gates, CLI razy tenant commands |
| Phase 4 — Kubernetes + Lifecycle | v1.3.0 | K8s namespace/PVC/NetworkPolicy templates, Helm chart, WorkerLifecycleManager integration |
| Phase 5 — Whitelist + Admin UI | v2.0.0 | TenantAccessPolicy, fine-grained cross-tenant data sharing, admin dashboard |
Phase 1 (isolation core) ─────┐
├──► Phase 2 (Docker) ──► Phase 4 (K8s)
│ │
└──► Phase 3 (L4 + Data) ─────┘──► Phase 5 (Whitelist)
Architecture deep-dive:
architecture/ENTERPRISE-TENANT-ISOLATION.md
Benchmarked against Laravel 12 + Octane (Swoole) under identical conditions — same host, same MySQL, same container resources (2 CPUs / 4 GB RAM), same k6 load profiles.
| Scenario | Razy RPS | Laravel RPS | Razy Advantage | Razy p95 | Laravel p95 |
|---|---|---|---|---|---|
| Static Route | 6,331 | 1,254 | 5.0× faster | 18.6ms | 186ms |
| Template Render | 6,264 | 1,137 | 5.5× faster | 19.0ms | 189ms |
| DB Read (SELECT) | 3,763 | 952 | 4.0× faster | 38.5ms | 191ms |
| DB Write (INSERT) | 754 | 842 | Laravel 1.1× | 182ms | 186ms |
| Composite (DB + Template) | 4,528 | 958 | 4.7× faster | 72.4ms | 395ms |
| Heavy CPU (500K MD5) | 144 | 325 | Laravel 2.3× | 595ms | 1,590ms |
Razy outperforms Laravel Octane in 4 of 6 scenarios — all throughput-dominant workloads. Laravel leads in DB Write (MySQL INSERT is the bottleneck, not framework overhead) and CPU-bound fast-request throughput (Swoole's coroutine isolation). Even in the Heavy CPU scenario, Razy achieves 2.7× lower tail latency (p95: 595ms vs 1,590ms).
| Razy | Laravel | |
|---|---|---|
| Runtime | FrankenPHP (Caddy, PHP 8.3.7, Alpine) | PHP 8.3-cli + Swoole (Octane) |
| Worker Mode | Persistent worker, boot-once dispatch | Octane Swoole (--workers=auto) |
| OPcache | JIT 1255, 128 MB buffer | JIT 1255, 128 MB buffer |
| Config Cache | N/A (standalone Phar) | config:cache, route:cache, view:cache |
The difference is architectural, not just runtime tuning:
| Razy | Laravel | |
|---|---|---|
| Per-request overhead | ~0.05ms (dispatch only) | ~0.8ms (service container resolution, middleware pipeline, route matching) |
| Object graph | Boot once, reuse across all requests | Rebuilt partially per request even with Octane |
| Template engine | Native PHP blocks, zero compilation | Blade compiles to PHP, then executes |
| Route matching | Direct hash lookup from pre-compiled table | Regex matching through middleware stack |
| Deployment | Single Razy.phar — nothing to cache |
Requires config:cache, route:cache, view:cache, event:cache for production |
Razy and Laravel solve different problems with fundamentally different philosophies:
| Aspect | Razy | Laravel |
|---|---|---|
| Design goal | Multi-project, multi-tenant module platform | Full-featured web application framework |
| Unit of work | Module (reusable, versioned, distributable) | Application (monolithic, project-bound) |
| Multi-site | First-class — distributors share modules | Bolted on via tenancy packages |
| Team boundary | Distributor per team, modules per sub-team, API/Event contracts | Package per team, service classes, facades |
| Upgrade model | Update shared module → all projects benefit | Update per project via composer update |
| Code sharing | Shared Modules (reference, not clone) | Composer packages (vendor lock per project) |
| Configuration | dist.php + package.php (minimal, flat) |
.env + config/*.php + service providers (layered, ceremonial) |
| Learning curve | Steep upfront (module lifecycle), low ongoing | Low entry (conventions), steep at scale (deep service container knowledge) |
| Ecosystem | Purpose-built, self-contained | Massive third-party ecosystem (Forge, Vapor, Nova, Livewire, etc.) |
Razy is ideal for:
- Multi-client platforms — agencies or SaaS providers maintaining many client projects on a single codebase
- Module-driven SaaS — products where each customer gets a different combination of features (modules)
- Subscription-based services — where continuous upgrades across all clients is a core business requirement
- High-throughput APIs — services where 5× throughput and 10× lower latency matter (real-time, IoT, fintech)
- Small teams managing many projects — one module update benefits every project simultaneously
- Microservice backends — lightweight, fast startup, single-binary deployment
Laravel is ideal for:
- Standalone web applications — CMS, e-commerce, admin panels with rich UI needs
- Teams that value convention over configuration — developers familiar with Rails/Django patterns
- Projects that rely heavily on third-party packages — authentication, billing, notifications, queues
- Prototyping and MVPs — rapid scaffolding with Artisan generators
- CPU-bound workloads — Swoole's coroutine model handles mixed I/O + CPU better
Full benchmark methodology, raw data, and reproduction steps:
benchmark/directory.
# Install dependencies
composer install
# Run the full test suite
composer test # 4,564 tests, 8,046 assertions (Windows — 87 skipped)
composer test-coverage # Generate coverage report
# Code quality
composer cs-check # Check PSR-12 compliance
composer cs-fix # Auto-fix code style
composer quality # Run tests + style checksTo run all tests with zero skips (including Redis, SSH2, and Linux-only permission tests):
docker compose -f .docker/docker-compose.test.yml up --build --abort-on-container-exit
# → 4,564 tests, 8,178 assertions, 0 skipped, 0 errorsTest suite covers: 102 test classes across Authenticator, Cache (File/Redis/Null), Collection, Configuration, Container (DI), Controller, Crypt, Database (drivers, queries, transactions, migrations), DOM, EventDispatcher, FTPClient, HashMap, HttpClient, Logger, Mailer, Middleware, Module system, ORM, Pipeline, Routing, SFTPClient, Session, Template, Validation, WebSocket, Worker lifecycle, YAML, and more.
New here? Start with the Quick Start (5 min) tutorial — build and run your first module in under 5 minutes.
Full documentation is available on the GitHub Wiki and the Documentation Site.
| Section | Topics |
|---|---|
| Getting Started | Quick Start · Installation · Architecture |
| Core Concepts | Modules · Controller · Agent · Routing · Events |
| Data & Storage | Database · Collection · Configuration · HashMap · YAML |
| Rendering | Template Engine · DOM Builder |
| IO & Communication | XHR · SSE · Mailer · FTP/SFTP · SimplifiedMessage |
| Security | Crypt · Authenticator |
| Advanced | Plugins · Threads · Simple Syntax |
| Deployment | Sites Config · Packaging · CLI · Caddy Worker |
| Reference | API Reference · ModuleInfo · Utilities · Testing |
Contributions are welcome! Please read the Contributing Guide before submitting a pull request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Ensure all tests pass (
composer test) - Submit a pull request
For bug reports and feature requests, please use GitHub Issues.
See also: Code of Conduct · Security Policy
Razy was born from real-world freelance experience managing multiple client projects simultaneously. Traditional frameworks made it painful to share code between projects, backport updates across deployments, and maintain version-specific module sets for different clients.
Razy solves this by treating modules as versioned, distributable units — each project (distributor) picks exactly which module versions to load, shared services are available globally, and the entire system packages into a single phar for deployment.
Design principles:
- Multi-tenancy by design — not bolted on as an afterthought
- Version isolation — different distributors can run different module versions side by side
- Zero-conflict autoloading — Composer packages are scoped per distributor
- One binary deployment —
Razy.pharcontains the entire framework
Razy didn't start as a framework. It grew out of years of real-world project delivery, each stage solving a pain that the previous one exposed.
In the early days of web development, PHP and HTML were tangled together. MVC was a good idea in theory, but in practice, frontend designers and backend developers constantly stepped on each other's toes. To reduce friction between the two roles, the first generation of Razy's template engine was born — inspired by phpBB's template architecture rather than Smarty, because Smarty's syntax was something a frontend person couldn't read at a glance. The goal was simple: give designers markup they can understand without learning a programming language.
When freelance projects started coming in, a painful pattern emerged. Every new project meant creating a new folder, copying in the template engine, configuring everything from scratch, and dragging over whatever libraries were useful from the last project. Each copy diverged the moment it was created. Changes were large, setup was slow, and nothing was truly reusable.
The natural next step was to consolidate common code into a shared library. But as that library grew, maintaining it became its own problem. Updating a feature in one project didn't transfer cleanly to another — it wasn't just copy-and-paste. Worse, working with other vendors' systems revealed a deeper structural issue: the earlier a client was onboarded, the more outdated their system became. Debugging old versions was expensive, shipping new features to legacy clients was impractical, and the business incentive for building new functionality dropped because only the newest clients would benefit.
This led to a fundamental rethink. Instead of treating each project as a standalone codebase, what if code management and project management were the same thing? What if every client's system was part of one unified development environment — different configurations of the same modules, not different copies? The concept of a module-driven, version-aware architecture started taking shape.
The business model evolved alongside the architecture. Rather than charging one-time project fees and walking away, the shift was toward monthly subscription services — maintaining a long-term relationship with each client. This meant every client, old and new, could receive continuous upgrades on a shared foundation. The economic incentive aligned perfectly with the architectural vision: invest once in a feature, roll it out across all subscribers.
Razy's first real prototype emerged with a clear mission: minimize the cost of developing and maintaining modules. Modules became reusable across projects. Multiple projects shared a single development environment. Module functionality was broken into small, focused fragments. URL-path-to-controller mapping made code navigation intuitive. Shared Modules could be referenced rather than cloned — one update propagated everywhere.
As projects grew, multiple teams began working on the same system. To prevent teams from interfering with each other's codebases, Razy introduced Module API and Event systems. Each team declared what data they needed via requirement requests, and the providing team exposed it through formal APIs and events. Cross-module communication became explicit and permissioned rather than implicit and fragile.
The unit of team ownership expanded. A team no longer managed just a single module — they managed an entire distributor, with sub-teams responsible for individual modules within it. This matched real organizational structures: one team owns the shop, another owns the admin panel, another owns the API gateway — each a distributor with its own routing, modules, and release cycle.
With multiple teams and multiple distributors sharing infrastructure, security became a priority. Razy went through continuous refinement to ensure modules couldn't reach across distributor boundaries and tamper with core logic. The architecture evolved to enforce isolation by default — not as a policy, but as a structural impossibility.
Through years of real project delivery, Razy was continuously refined. Features like Simple Syntax (a shorthand for complex SQL joins and JSON operations) were added to make everyday development faster and more intuitive. Each pain point encountered in production fed back into the framework's design.
The modern development environment demanded more. FrankenPHP Worker Mode was integrated for persistent-process performance. Mainstream deployment patterns (Docker, Caddy, CI/CD) were supported natively. AI tooling was adopted to accelerate documentation, code auditing, and architectural analysis — turning months of manual work into days. After three years of development plus six months of AI-assisted refinement, Razy v1.0-Beta shipped. Benchmarks showed higher throughput, lower latency, and better resource efficiency than mainstream frameworks like Laravel.
To minimize the impact of hotfixes and upgrades on running systems, Razy introduced Core Container and Module Container concepts. As long as a worker process was serving requests, hotplugged updates could be staged and transitioned using configurable strategies — from graceful drain to immediate swap — enabling zero-downtime version transitions in production.
The latest evolution brings enterprise-grade tenant isolation. Each tenant runs as an isolated pod with its own filesystem, data, and configuration — supporting both Docker Compose and Kubernetes deployments. Staging environments are cleaner, SaaS onboarding is streamlined, and microservice architectures can leverage Razy's module system without sacrificing security boundaries. The framework now supports the full spectrum from single-developer side projects to multi-team, multi-tenant enterprise platforms.
MIT License — Copyright (c) Ray Fung