The modular SaaS engine
for
Laravel
Multi-tenancy, OAuth, permissions, and more - battle-tested through a production CRM-ERP. So you never rebuild the same SaaS foundation again.
# Install LaraFoundry
$ composer require larafoundry/core
# Scaffold your SaaS
$ php artisan foundry:install
✓ Multi-tenancy configured
✓ Authentication scaffolded
✓ Permissions seeded
✓ Admin panel ready
Your SaaS foundation is ready.
What is LaraFoundry?
LaraFoundry is a modular SaaS engine built with Laravel 12, designed to give developers a head start when building multi-tenant SaaS applications.
Instead of rebuilding the same features for every project, LaraFoundry provides a battle-tested foundation that handles the complex parts - so you can focus on building what makes your product unique.
LaraFoundry is being actively developed and battle-tested through Kohana, a production SaaS CRM-ERP system. The core will be extracted and released as an open-source package once fully validated.
Everything you need to ship faster
Multi-Tenancy
Complete tenant isolation with company-scoped models and database-level separation.
Authentication
OAuth integration, 2FA, QR code login, and comprehensive session management.
Permissions
Flexible role-based access control with granular permissions system.
Modern Stack
Laravel 12, Vue 3, Inertia.js v2, Vite Build Tool and Custom SCSS.
Testing
Comprehensive test coverage with Pest PHP for reliable deployments.
Admin Panel
Full-featured admin interface with user management and activity logging.
User Management
Manage users across tenants with the ability to follow into any user account for impersonation and support.
Logging & Monitoring
Comprehensive activity logging for users and system-wide events with a convenient audit trail UI.
Feature Requests & Voting
Built-in feedback board where users can submit feature requests, upvote ideas, and track implementation status.
Support Ticketing
Integrated helpdesk module with ticket creation, assignment, priority levels, and status tracking.
Payments
Flexible billing module supporting subscriptions, one-time payments, and invoice management.
Affiliate Program
Built-in referral and partner tracking with commission management and payout reporting.
Multi-Language
Full i18n support with per-tenant locale settings, translation management, and RTL-ready layouts.
Security & Access Control
OAuth2, 2FA, rate limiting, IP allowlists, and automatic logging of unauthorised access attempts.
Notification System
Transactional and user-triggered notifications via email, in-app, and webhook channels with template management.
Custom Documentation
Embeddable docs module that lets tenants create and publish their own knowledge base or product documentation.
Free to Use, Fair to Build
LaraFoundry is open source and free for personal projects, learning, and non-commercial use. If you're using it to power a commercial product or service, a commercial license is required.
Personal Use
Free- Side projects & experiments
- Learning & education
- Open-source projects
- Non-commercial apps
Commercial Use
License required- SaaS products & startups
- Client projects
- Internal business tools
- Any revenue-generating use
Latest Updates
v0.10.0: Admin Companies console
The second screen of the operator panel, after admin users. An operator can suspend a whole tenant now. Blocking the company was the easy half; the half worth building carefully was not locking out the people who belong to other companies too. The donor had no company-level block at all: to cut off a tenant you banned its owner and hoped the side effects held.
- A company block is one column - a single non-mass-assignable timestamp on the company, set behind an operator policy and written to the activity log, instead of the donor's "ban the owner and infer the rest"
- Enforced at one boundary - the block is read in the single middleware every tenant-scoped request already passes through, so one column takes the whole tenant down and there is nothing to forget on the next route you add
- It self-heals instead of locking out - a user can belong to several companies. Suspend their active one and the middleware promotes them into the next un-blocked company and replays the request, or shows a suspended screen. Never a logout, never a redirect loop
- Read-only subscription status - the console shows each tenant as on trial, active, expiring, expired or never activated, computed from the free core's billing columns. Managing subscriptions is the paid add-on, on purpose
- The review earned its keep again - it caught a redirect loop in the enforcement and a promotion query that switched users straight back into the blocked company. Both fixed, with regression tests
v0.9.0: Billing seam
The honest headline: this ships the billing seam, not billing. The free core now has the whole shape of a subscription system, a payment-gateway contract, a driver manager, a real access gate, and it cannot take a single cent. On purpose. The day a business charges its customers is the paid part, so billing had to split down a clean line, with the free side carrying real structure and the paid add-on carrying the parts that move money.
- A gateway contract, no payment SDK - the free core describes subscribe, cancel, refund and webhook verification, and ships one driver: a null gateway that refuses every money operation loudly. No Stripe dependency anywhere
- The donor's hardcoded success did not survive - the original CRM faked payment with $paymentStatus = 'success'. A success that is always true looks like billing works, so it was the one thing not extracted
- Gateway-agnostic by design - real drivers (Stripe, Paddle, a local PSP) are registered by the paid add-on through a Mail/Queue-style manager. Swapping is one config value, which matters most outside the ~46 countries Stripe reaches
- A real access gate, fail-closed - hasAccess() was a stub returning true since the RBAC phase. It now reads subscription columns: open while billing is off (the free promise), fail-closed on a live trial or active subscription when on. Those columns are not mass-assignable
- Honest about scope - the gate is wired but no caller enforces it yet, and there are zero real payments, plans, or portal here. That is the paid add-on, a later milestone. What ships is the contract they stand on
v0.8.0: File & Media library
The last piece of infrastructure before billing: a file layer where everything goes through a configured disk via Storage::disk(config), never public_path() like the donor CRM did. That one change makes avatars and logos S3-portable with no code edits, and closes a class of donor habits at once: files written into the deployable web root, a hardcoded disk on the company logo, and an avatar column that mixed a stored path with an OAuth provider's URL.
- One storage contract, disk from config - uploads land through Storage::disk(config) with a generated uuid filename under a date-sharded path. Switch the config from public to s3 and every avatar and logo moves to the bucket, no call site changes
- An avatar column that meant two things - a stored path for uploads, an absolute URL for OAuth sign-ins. The resolver tells them apart instead of double-prefixing the external URL into a dead link
- A missing avatar costs zero files - the default is drawn inline as an SVG data URI, so a user with no upload produces nothing to store, orphan, or prune, and needs no image extension
- Private files behind a signed door - a private disk reached only through a short-lived signed URL plus a server-side disk check, so a document never leaks by guessable path. The seam for order and invoice documents in a later phase
- The media library itself is a seam, not a table - the storage contract and DTO are shaped so a polymorphic media table and a HasMedia trait drop in later without rewriting call sites. Same deferred-seam move as before billing
v0.7.0: Navigation & Admin Users
A permission-aware navigation engine and the first real screen of the operator console, built together. The menu is built and filtered on the backend, so links a user can't reach never reach the browser. And rebuilding impersonation surfaced a one-line hole that had been live in the original CRM the whole time: any admin could log in as any user, with no record of it.
- Backend-filtered menu - items are collected, permission-filtered and sorted server-side; the frontend only renders what survives, so the full menu never leaks into the page payload
- Host extends with a provider - the host adds its own menu items through a class it registers, the same additive seam used for permissions and activity-log events, not a hardcoded array
- Labels are i18n keys, not strings - the menu ships translation keys and Vue translates at render, so the language switcher repaints the menu live instead of on reload
- Admin user management - list, search, edit, block, soft-delete and restore, on the v0.1.0 UI kit. Blocking now kills the user's tracked sessions so it bites immediately
- The impersonation hole, closed - the donor's canImpersonate() check was commented out, so any admin could impersonate anyone, unlogged. Replaced with a real policy: super-admin only, never another admin, never blocked/deleted, and both take and leave are audited
v0.6.0: Multilanguage
A language switcher and a second language, bolted onto the locale machinery that already shipped in the foundation release. Half of i18n was always there: the detection chain, the allow-list that keeps junk codes out of the database, the contract for the user's locale. This phase added the visible half, and the boring feature still had two real bugs.
- Language switcher - a dropdown driven by a shared list of available locales, and a CSRF-protected switch route open to guests and signed-in users alike
- English and Ukrainian, out of the box - the core's own screens ship translated; a host overrides any string from its own lang files and adds its own languages
- An open redirect, caught by a test - the switch sent users "back" via a helper that trusts the forgeable Referer header. Fixed: the return URL is constrained to the app's own host
- A placeholder in the wrong dialect - the review caught a heading written with Laravel's :name syntax inside vue-i18n, whose syntax is {name}, so a company name silently never appeared
v0.5.0: Activity Log
A platform audit trail, lifted from the same production CRM and built on spatie/laravel-activitylog. The point isn't the storage, it's the context: every entry carries the device, IP, route and geolocation of the request behind it, and a single super-admin viewer reads all of it. This is a platform-operator tool, not a tenant feature, so the log is global and there is nothing per-company to get wrong.
- Context on every entry - device, IP, route, HTTP method and async geolocation, wrapped around spatie rather than reinventing it
- Distinct causer and subject - who did it vs what it was done to, never the same id in both, with PII redacted from the stored URL before it lands
- A config event registry - the core logs its own auth and tenancy events; the host adds domain events the same way it extends the permission catalog
- Super-admin viewer - global and per-user views, an hours filter and request detail, gated by the identity flag from the auth layer, on the v0.1.0 UI kit
- Model auditing, fixed - an opt-in trait that records real created / updated / deleted diffs, decorated with the same request context
- The review earned its keep again - it caught the log recording the wrong subject for events that carry more than one model. Fixed: the specific object wins over the surrounding context
v0.4.0: Roles & Permissions
Role-based access control inside a company, lifted from the same production CRM. Self-written, not Spatie, and tenant-scoped from the ground up: every role and grant carries the company it belongs to. The hook left in the tenancy release now fires, seeding default roles the moment a company is created.
- Tenant-scoped RBAC - roles, per-user grants and revokes, all scoped by company. A permission in one company never leaks into another
- A check priority that reads like a sentence - super-admin, then company owner, then an explicit revoke, then a grant, then roles. Revoke always beats grant
- Owner and super-admin bypass - the owner flag from the tenancy layer is the only company bypass; super-admin is an identity flag, never a role
- Default roles on company creation - a template role set is cloned into every new company through the event the tenancy release exposed
- Roles UI - create and edit company roles with a grouped permission picker, on the v0.1.0 UI kit
- The review earned its keep again - it caught a privilege-escalation hole where a delegated member could hand out more than they held. Fixed: you can only grant what you already have
v0.3.0: Multi-Tenancy
The company / team layer is in the package, lifted out of the same production CRM. One database, many tenants, hard walls between them. The module where re-reading the old code paid off the most: the original isolation scope was fail-open, and the package version fixes it.
- Fail-closed tenant scope - no resolvable tenant returns zero rows, never every row. Fixes a legacy IDOR where a missing active company leaked the whole table
- Two modes, one scope - teams (tenant is a Company) or personal (tenant is the User), behind a config switch, sharing the same global scope and trait
- Resolver behind an interface - the active company is read per-device from the session today, ready for an API-token resolver later without touching call sites
- Company creation wizard - create, switch active company, manage employees, all on the v0.1.0 UI kit
- Invitations with an email-ownership guard - a leaked token can't join a company unless you prove you own the invited mailbox
- Tests, and a second review - full Pest + Vue coverage, and a second code-review pass found two real security holes the first one walked past
v0.2.x: Authentication & Users
The first domain module is in the package. Built on top of Laravel Fortify, not a port of my old hand-rolled auth, with the pieces Fortify doesn't cover added around it.
- Fortify core - login, registration, password reset, email verification, and real per-user two-factor (TOTP + recovery codes + QR), the official way
- Session tracking - one row per device with a fingerprint, IP and last activity, powering an active sessions view and "log out other devices"
- OAuth via Socialite - social sign-in with an account-takeover guard that refuses to link a provider to an existing local account by default
- Blocked / deleted gate - a per-request middleware that logs out an account the moment it's blocked
- Localized auth mail - verification and reset emails owned by the core, translated, no hardcoded English
- Inertia + Vue pages - login, register, reset, verify, 2FA challenge and settings, on the v0.1.0 UI kit
- 114 tests (backend + frontend), and the integration caught two real bugs the unit tests didn't
v0.1.0: The Core, As a Package
LaraFoundry turned from a plan into an installable Composer package. The first tag ships the foundation layer, the cross-cutting primitives every later module stands on, extracted from the production CRM and hardened on the way out.
- Locale engine - one validated resolution chain (preference, session, cookie, browser, optional geo, default) with a single allow-list, so junk locale codes can't reach the app
- Query filters - a reflection-based filter base, hardened against method injection from the query string
- Core middleware - email verification, IP allow-list, intended-URL capture, appearance
- Inertia + Vue UI kit - i18n bootstrap, form fields, flash toasts, paginator, base layout, running cleanly from vendor/
- 39 Pest tests, CI green on PHP 8.2 / 8.3 / 8.4, and the extraction caught two real production bugs
The billing add-on, and the revenue views it unlocks
The operator console can list, inspect and suspend tenants now, and read their subscription status. What it can't do is change that status, because moving money is the paid part. Next is the larafoundry-billing add-on, a separate package that plugs into the seam: real Stripe and Paddle drivers, plans, subscriptions, promo codes, the merchant-of-record flow, and the wiring that finally makes the access gate enforce itself. With real subscription data behind it, the revenue dashboard follows.
Coming Soon