A modular, self-hosted CMS and eCommerce platform built on Laravel 12.
Documentation | Themes | Issues | Contact
Open source under the MIT licence: use it commercially, fork it, build closed-source themes and plugins on top of it. Security issues go through SECURITY.md, not the public issue tracker.
Every optional feature is a module you can switch off from the admin panel — a disabled module registers no routes and runs no queries.
| Requirement | Minimum |
|---|---|
| PHP | 8.2 or newer |
| MySQL / MariaDB | 5.7+ / 10.3+ |
| PHP extensions | pdo_mysql, mbstring, openssl, tokenizer, json, curl, fileinfo, zip, gd, xml, ctype |
| Writable | storage/, bootstrap/cache/, themes/, public/, .env |
The setup wizard checks all of this for you before it will continue.
- Upload the files. Point your domain at the
public/directory if your host allows it; otherwise point it at the project folder and the included.htaccessserves the site from there (never serve the folder without it). - Create an empty MySQL database.
- Open your site in a browser — the setup wizard starts automatically.
- Follow the four steps: server check → database → site details → admin account.
On nginx (including CloudPanel), .htaccess is not read at all. Use the
included nginx.conf.example, or set the document root to public/. Full
notes are in the documentation.
cp .env.example .env # then fill in your DB_ credentials
php artisan key:generate
php artisan migrate --force
php artisan db:seed --force
php artisan cms:admin # creates your admin account
php artisan storage:link
echo "{}" > storage/installed- Turn on two-factor authentication for your account.
- Switch off modules you don't need under System → Modules.
- Set your email provider under Settings → Email.
- Configure a payment gateway under Shop → Payment gateways, if selling.
- Set
APP_DEBUG=falseandAPP_ENV=productionin.envbefore going live.
System → System shows a checklist of anything still misconfigured.
| Module | What it adds | Required |
|---|---|---|
| Pages | Static pages, templates, homepage selection | Yes |
| Media | Uploads, thumbnails, file manager | Yes |
| Themes | Upload and activate front-end templates | Yes |
| Users | Accounts, roles, two-factor auth | Yes |
| Blog | Posts, categories, tags, comments | No |
| eCommerce | Products, cart, checkout, orders, coupons | No |
| Payments | PayPal, Stripe, Razorpay, PayU, Cashfree, Wise, COD, bank transfer | No |
| SMS | Transactional SMS and OTP via MSG91 or Twilio | No |
| Firebase | Web push notifications | No |
| SEO | Meta tags, sitemap.xml, robots.txt, schema.org, redirects | No |
| Contact forms | Front-end form and submission inbox | No |
| Newsletter | Subscriber capture and CSV export | No |
| Import & export | Content bundles in and out, plus a WordPress importer | No |
| Mobile API | JSON API for a mobile app — ships switched off | No |
Dependencies are handled for you (e.g. switching off Payments also switches off eCommerce), and nothing is ever deleted when a module is turned off.
| Gateway | Flow | Refunds from admin | Webhooks |
|---|---|---|---|
| Stripe | Hosted Checkout redirect | Yes | Yes |
| PayPal | Orders v2 redirect | Yes | Yes |
| Razorpay | Checkout modal | Yes | Yes |
| PayU | Signed form POST | Yes | No |
| Cashfree | Hosted checkout | Yes | Yes |
| Wise | Bank transfer, with optional transfer matching | No | No |
| Cash on delivery | Offline | No | No |
| Bank transfer | Offline | No | No |
Credentials are encrypted at rest and never sent back to the browser. Gateways
talk to their providers over REST, so no vendor SDKs are pulled into vendor/.
A JSON API for a mobile app — content, shop and customer accounts. It's the only module that ships switched off. Every request must carry a registered app's key/secret; there is no public or unauthenticated mode.
X-Api-Key: rad_9f3c… the app's public key
X-Api-Secret: … its secret, generated in the admin panel
Authorization: Bearer … the customer's token, for their own data
Endpoint groups (sign-in, shop, blog, reviews, search, newsletter, push, etc.) are switched on individually under Mobile API → Endpoints — anything not enabled answers 404. Optional HMAC request signing, rotating refresh tokens, device management and 2FA are all supported. Paying from an app hands off to a signed link that uses the same web checkout flow as the browser.
See the documentation for the full API reference.
SMTP settings live in the admin panel. API-key providers read their keys from
.env instead:
| Provider | .env keys |
Package to install |
|---|---|---|
| SMTP | — | built in |
| Resend | RESEND_KEY |
composer require resend/resend-laravel |
| Amazon SES | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION |
composer require aws/aws-sdk-php |
| Postmark | POSTMARK_TOKEN |
composer require symfony/postmark-mailer |
A drag-and-drop editor for designing pages without writing HTML — built into the CMS, not bolted on. Pages are a tree of section → column → widget, rendered server-side as plain Blade/HTML.
- Themes opt in per region with
@region('header') ... @endregion; a theme with no regions is unaffected and the builder still works on page content. - 27 built-in widgets across Basic, Media, Layout, Content, Shop and Site parts.
- Style edits patch the live stylesheet instantly; content edits re-render just that element. The server recompiles the real stylesheet on publish.
- Drafts autosave, publishing keeps the last 25 revisions, and editing is responsive per breakpoint with design tokens for colours.
- Builder-authored CSS and markup are sanitised (no
javascript:/data:URLs, no rule-breaking characters, capped tree size).
Adding a widget is one PHP class plus one Blade view — see the documentation for a walkthrough.
Themes live in themes/<slug>/ and install as a .zip from
Appearance → Themes, or straight from Appearance → Browse themes
(a hosted, free directory with search and one-click install/update).
my-theme/
├── theme.json required: name, version
├── screenshot.png
├── assets/ copied to public/themes/<slug>/
└── views/ required — anything omitted falls back to the default theme
Every upload is scanned before anything is written to disk: zip-slip and
zip-bomb protection, an extension allowlist (no .php, .htaccess, .sh),
and a Blade scan that rejects raw PHP, eval, shell exec or filesystem writes.
Directory installs go through the same scan, over HTTPS, with a checksum and
slug match required.
Every upload (media library, editor, builder) is checked before it's saved: extension and file contents must agree, double extensions and embedded PHP are rejected, SVGs are sanitised, and photos are re-encoded to strip EXIF/GPS data. The uploads and theme-assets folders never execute PHP, on Apache or nginx.
Only public/ should be on the web. The included .htaccess keeps the
project root private even if the whole project sits inside the web root; on
nginx, set the document root to public/ or deny the app folders explicitly.
System → Security can probe your live site for .env and other files that
should not be reachable.
Digital products are stored outside the media library, in
storage/app/private/downloads/ with no public URL. Downloads are only served
through /account/orders/{order}/download/{item}, which verifies the order,
payment status and download limit before streaming the file as an attachment.
CMS_DOWNLOADS_DISK can move storage to S3 or elsewhere (keep it private).
The CMS can update itself. Point CMS_UPDATE_URL at a JSON manifest (it
defaults to this repo's GitHub releases), and updates are offered under
System → Updates — installing is always a manual click.
# 1. Bump the version in config/cms.php
git commit -am "Release 1.1.3"
# 2. Tag it — the message becomes the release description
git tag -a v1.1.3 -m "Fixes a stored XSS in the media library.
tags: security"
# 3. Push both
git push && git push --tagsGitHub Actions builds, packages and checksums the release automatically. Every
release is verified before install (HTTPS, SHA-256, version match) and only
files on an allowlist are ever written — .env, uploads and other themes are
never touched. A backup is taken before every update, with one-click rollback.
See the documentation for manifest format and self-hosting a release feed.
- Two-factor authentication (TOTP) with encrypted secrets and one-time recovery codes.
- Money stored as integers (minor units); order totals always recalculated server-side.
- Stock decremented inside the order transaction to prevent overselling.
- Order numbers are unguessable; paid downloads are never served by URL.
- Release archives and theme installs are checksum- and HTTPS-verified.
- Payment returns are always re-verified against the provider's API.
- Login is rate-limited per email and IP.
Full details are in SECURITY.md and the documentation.
php artisan cms:admin # create or promote an admin account
php artisan cms:sync # register new modules/gateways/settings after an upgrade
php artisan cms:sync --themes # re-scan the themes folder
php artisan cms:demo # install sample posts, pages and products
php artisan cms:demo --remove # delete that sample content again
php artisan cms:export # export content as a portable bundle
php artisan cms:import <file> # import a Radius export bundle
php artisan cms:import-wordpress <file.xml> --media # import a WordPress export
php artisan cms:marketplace-entry <zip> --url=<folder> # print a theme directory listing for a ZIP
php artisan cms:release # build a distributable release ZIP
php artisan optimize:clear # clear all cachescomposer install
npm install
npm run dev # Vite dev server with hot reload
npm run build # compile assets for productionCompiled assets are committed under public/build/, so a buyer never needs
Node installed to run the site.
Radius ships with its own logo and favicon set; uploading your own under
Settings → General overrides it everywhere. For a white-label fork,
repoint the brand block in config/cms.php instead of editing views:
'brand' => [
'logo' => 'images/radius-logo.png',
'logo_light' => 'images/radius-logo-light.png',
'favicon' => 'favicon/favicon.ico',
'favicon_light' => 'favicon-light/favicon.ico',
],Use the <x-site-logo> component in views — it resolves light/dark ink from
the site's colour scheme automatically.
app/
├── Cms/ the CMS itself, kept apart from Laravel's skeleton
│ ├── Settings/ database-backed settings with caching
│ ├── Modules/ the module on/off switchboard
│ ├── Themes/ theme discovery, activation and the ZIP installer
│ ├── Marketplace/ the theme directory: catalogue, install, updates
│ ├── Payments/ gateway contract, result object and drivers
│ ├── Sms/ SMS drivers and the one-time-code flow
│ ├── Firebase/ FCM push and client config
│ ├── Seo/ meta, schema.org and sitemap generation
│ ├── Shop/ cart and order services
│ ├── Builder/ the visual editor's engine
│ │ └── Blocks/ one class per widget
│ ├── Media/ uploads and thumbnails
│ └── Support/ helpers, 2FA, admin navigation
├── Http/Controllers/
│ ├── Admin/ the admin panel
│ ├── Front/ the storefront
│ ├── Auth/ sign-in, registration, two-factor
│ └── Installer/ the setup wizard
└── Models/
config/
├── cms.php modules, themes, media, security, brand assets
├── settings.php every admin setting, declared once
├── payments.php gateway definitions and endpoints
└── builder.php registered widgets, breakpoints, design tokens
themes/default/ the bundled starter theme
Adding a setting is one array entry in config/settings.php. Adding a payment
gateway is one driver class plus one entry in config/payments.php. Adding a
builder widget is one block class, one Blade view, and one entry in
config/builder.php.