In Development

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.
About

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.

Current Status

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.

Core Features

Everything you need to ship faster

01

Multi-Tenancy

Complete tenant isolation with company-scoped models and database-level separation.

02

Authentication

OAuth integration, 2FA, QR code login, and comprehensive session management.

03

Permissions

Flexible role-based access control with granular permissions system.

04

Modern Stack

Laravel 12, Vue 3, Inertia.js v2, Vite Build Tool and Custom SCSS.

05

Testing

Comprehensive test coverage with Pest PHP for reliable deployments.

06

Admin Panel

Full-featured admin interface with user management and activity logging.

07

User Management

Manage users across tenants with the ability to follow into any user account for impersonation and support.

08

Logging & Monitoring

Comprehensive activity logging for users and system-wide events with a convenient audit trail UI.

09

Feature Requests & Voting

Built-in feedback board where users can submit feature requests, upvote ideas, and track implementation status.

10

Support Ticketing

Integrated helpdesk module with ticket creation, assignment, priority levels, and status tracking.

11

Payments

Flexible billing module supporting subscriptions, one-time payments, and invoice management.

12

Affiliate Program

Built-in referral and partner tracking with commission management and payout reporting.

13

Multi-Language

Full i18n support with per-tenant locale settings, translation management, and RTL-ready layouts.

14

Security & Access Control

OAuth2, 2FA, rate limiting, IP allowlists, and automatic logging of unauthorised access attempts.

15

Notification System

Transactional and user-triggered notifications via email, in-app, and webhook channels with template management.

16

Custom Documentation

Embeddable docs module that lets tenants create and publish their own knowledge base or product documentation.

License

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
Building in Public

About the Author

Hi! I'm building LaraFoundry to solve a problem I've encountered many times: re-implementing the same SaaS foundation for different projects.

I'm documenting the entire journey - from architecture decisions to implementation challenges - sharing real code, real problems, and real solutions.

Changelog

Latest Updates

June 2026

v0.17.0: Legal & GDPR layer

The legal and GDPR layer, built as a seam rather than a checkbox. The honest find was that the right to access and the right to be forgotten are the same shape: two mirrored provider registries, so every module wires data export and account erasure the same way. Free core, so the code is open.

  • Versioned legal pages - a super-admin editor for Terms, Privacy and Cookie policy, stored per locale and served on a public /legal/{slug}. The body is sanitized on save and on render through the same sanitizer the email editor uses, and a fail-closed registry decides which slugs exist, so an unpublished page 404s instead of serving a placeholder as real legal text
  • Consent that ships off - the core sets only strictly-necessary cookies, so the banner is off by default. A registration Terms checkbox and a re-accept gate that fires only when the published Terms version is bumped, all reading one ConsentManager so they never disagree. Fail-open until a Terms page exists, so a fresh install is never locked out
  • The right to access - a synchronous JSON export of everything the app holds about a user, assembled from every registered export provider (profile, sessions, settings, consent; modules add their own), rate-limited against repeated dumps
  • The right to be forgotten, reversibly - deleting an account is a soft-delete that starts a grace clock a super-admin can reverse. A daily command then anonymises the identity rather than hard-deleting it, so foreign keys and the audit trail survive. Idempotent via a stamp; the activity log is kept on purpose, anonymise the who and keep the what, as proof the erasure ran
June 2026

v0.16.0: Settings, profile & email templates

Three small service modules most SaaS apps rebuild by hand, shipped together: a generic settings store, a self-service profile hub, and a database editor for the wording of the core's transactional emails. The email editor is the one with teeth, because letting an admin edit an email body is one keystroke away from handing them code execution.

  • A fail-closed settings store - one generic key-value store with three scopes (app, company, user). Only keys declared in a config registry can be read or written, each cast and validated against its rule; company settings are RBAC-gated and scoped to the active company server-side
  • A profile hub on one page - name and email, password, two-factor, PIN, sessions, avatar and UI preferences. Changing the email asks for the current password, resets verification and revokes other sessions. UI preferences pass through an allowlist into the column the donor let any key into
  • Email templates that cannot run code - a super-admin edits the subject and HTML body per locale, but the renderer is a single-pass token replace, never Blade or eval, so a stored template can't execute code. On top: a strict allowed-variable check (422 otherwise), an HTML purifier on the body, and a sandboxed-iframe preview. A deactivated template falls back to the static wording, so mail never breaks
June 2026

v0.15.0: Support tickets (helpdesk)

A support channel between a host user and the platform operator: the customer opens a ticket, the operator answers from the console. Extracted from the production donor and rewritten as a self-contained module, with the external ticket package deleted on purpose.

  • A queue on both sides - every user reaches their own tickets from a header Support link, opens one and replies; the operator works a queue with filters and counters. The user list is scoped to the caller, and a blocked user can still reach support, because it is their only channel to the operator
  • Status you never pick by hand - it is derived from the action: a user opens to wait-moderator, an operator reply moves it to wait-customer, a user reply reopens it, and the operator closes it to resolved
  • Audited and notified - every operator mutation lands on the activity log, and a reply or an operator-opened ticket pushes an in-app notification to the author through the v0.14.0 notification seam
  • The donor's XSS hole, closed - message bodies render as text, never v-html; creation and replies are rate-limited; categories and labels are config-driven JSON slug lists, so there are no extra tables
June 2026

v0.14.0: Notifications

An in-app notification centre plus super-admin broadcasts, delivered without realtime infrastructure. It polls instead of opening WebSockets, so nothing on shared hosting needs a daemon to run it.

  • A per-user inbox - a bell with an unread badge polled on a light, visibility-aware interval, and a full inbox page. Every query is scoped to the caller, titles and bodies render as text not v-html, and a notification's actions are reduced to internal GET links
  • Broadcasts that don't block the request - the operator drafts a per-locale message with an audience filter and an optional window; sending is a queued, chunked fan-out, not a synchronous mass-attach, so a large user base never blocks or double-delivers. The send route is rate-limited and the super-admin is excluded
  • A seam for domain events - a host pushes a system notification through one service, the seam that replaces the donor's per-event jobs. A prune command clears read notifications past the retention window, scheduled like the activity-log clean
June 2026

v0.13.0: QR cross-device login + presentation switch

The end of the auth-entry phase: a WhatsApp-Web-style sign-in lifted from the donor with its holes closed, plus a config switch for how the guest auth screens are surfaced. The extraction closed eleven security holes before it merged.

  • QR login, hardened - the web side shows a QR, a signed-in device scans and approves, the web side polls until it is in. The token is SHA-256 hashed in the DB (plaintext only in the QR), every endpoint rate-limited, the session regenerated before login, the code single-use under a sliding TTL and an absolute cap, attempts audited. The scanner validates the decoded URL before any request, closing the donor's blind-fetch SSRF
  • One guard-agnostic endpoint - the verify route runs on auth:sanctum, so a same-origin web request authenticates by session cookie today and a future native app by bearer token, through one controller. The core installs Sanctum and carries the access-tokens table
  • A page-or-modal switch - one config value renders the same Login, Register, Forgot and Reset screens as a full page (the unchanged default) or an overlay, driven by one shared prop. An unknown value falls back to page, so the default surface never breaks
June 2026

v0.12.0: Auth entry modes (super-admin gate + PIN-lock)

Hardens how the platform operator enters the console, and adds an optional session PIN-lock for everyone. Three guards, each a no-op for the users it does not apply to, so they sit safely on the global web group.

  • A reserved super-admin identity - the operator email cannot register or own a company, and a redirect middleware keeps the operator inside /admin, out of the tenant routes
  • Operator login is OTP-only, on every channel - an OTP step-up gate sits on the console: the operator must have confirmed two-factor and clear a fresh OTP once per session before /admin opens. An OAuth login skips the normal login challenge, but this gate catches it, and a stolen cookie, uniformly
  • A session PIN-lock - an idle session auto-locks and bounces to a PIN screen, a Telegram-style quick re-entry instead of a full re-login. PIN is per-user, lock state is per-session (lock everywhere, unlock per-device), and entry is rate-limited, the brute-force hole the donor lacked
June 2026

v0.11.0: Admin Dashboard

The operator console's landing screen, and the widget seam behind it. The dashboard is the exact mirror of the navigation engine: the backend builds and permission-filters a widget list, and Vue renders each one from a pluggable registry. It is revenue-agnostic on purpose.

  • A widget seam, 1:1 with the menu - providers contribute widgets for a level, the builder merges, RBAC-filters, sorts and memoises them; a host or the paid add-on adds widgets without editing the core. Titles are i18n keys, translated in Vue
  • Metrics that stay O(1) - every figure is a constant-query aggregate, never a per-row classification, so the page does not slow down as users and companies grow. Users, companies (with the subscription-status breakdown reproduced in SQL) and a 24h activity feed
  • Degrades gracefully - the frontend resolves each widget's component through a registry and falls back to a raw-data widget for a name it does not know, so a missing add-on registrar never crashes the page. Revenue is deliberately not here; the revenue widget plugs into the same seam from the paid add-on
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
June 2026

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
Next Up

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

Stay Updated

Get notified about major updates, new features, and technical deep-dives. No spam, just the good stuff.

Your email will only be used for LaraFoundry updates. No spam, ever.