Skip to content

Start building your next app or website — with AI, right in your browser.

Visit devdojo.com
Docs
Foundation

Apps (Coming Soon) 04 / 32

Foundation

The metapackage that bundles the first-party DevDojo feature packages and adds a runtime feature-flag system on top, so any app can turn capabilities on and off without touching code.

devdojo/foundation is the layer that ties the DevDojo Foundation packages together. It is a thin metapackage: it requires the active feature packages, ships a small foundation_settings table plus a /foundation/setup screen, and exposes one runtime feature-flag API — Devdojo\Foundation\Foundation::enabled('billing') — that every feature package gates its own boot on.

Install the Foundation and you get a complete SaaS stack — auth, billing, accounts, teams, blog, changelog, notifications, components — wired up and on by default. Disable whatever you don't need, per app, at runtime, from a database-backed settings table rather than a config edit and a redeploy.

What the Foundation bundles

The package's composer.json requires the whole first-party stack, so a single composer require devdojo/foundation pulls all of it in:

Package Role
auth Authentication — login, register, verification, reset, social, 2FA.
billing Subscriptions, checkout, plans, invoices, customer portal.
accounts Clerk-style account area (profile, avatar, email, 2FA, sessions, billing).
teams Teams, memberships, roles, and invitations.
notifications In-app database notifications and preferences.
blog Drop-in blog (posts, categories, admin).
changelog Drop-in changelog with read-tracking.
components The Blade UI component library.

The service provider is registered automatically via package discovery (Devdojo\Foundation\FoundationServiceProvider), and everything under the package autoloads from the Devdojo\Foundation\ namespace. The database browser is a sibling package that pairs well with the stack but isn't bundled — require it separately.

Requirements

  • PHP 8.2+ and Laravel 11, 12, or 13.
  • Each bundled package brings its own dependencies (Livewire, Volt, Folio, Socialite, Spatie permissions, Stripe, …) via Composer — one composer require resolves the whole stack.

Installation

Require the package, then run the installer:

composer require devdojo/foundation
php artisan foundation:install

The foundation:install command (Devdojo\Foundation\Commands\InstallCommand) does four things in order:

  1. Publishes config/foundation.php (tag foundation-config).
  2. Runs migrate --force for the entire stack — the Foundation's own foundation_settings table plus every feature package's migrations.
  3. Seeds the settings table with the default flags using firstOrCreate, so existing overrides are never clobbered.
  4. Best-effort storage:link.

Pass --force to overwrite an already-published config. When it finishes, it points you at /foundation/setup to choose which features are active.

Two publish tags exist if you'd rather publish pieces yourself: foundation-config (the config file, which the installer publishes for you) and foundation-assets (the package images, copied to public/vendor/foundation/images — not published by the installer).

Note — The foundation_settings migration is always loaded by the service provider (loadMigrationsFrom), not only when published. The feature-flag store exists wherever the Foundation is installed, even before you run vendor:publish.

Configuration

config/foundation.php has two keys. features holds the default on/off state for each bundled package, and depends declares prerequisites that are auto-enabled.

return [
    'features' => [
        'auth' => true,  // foundational — effectively always on
        'billing' => true,
        'blog' => true,
        'changelog' => true,
        'notifications' => true,
        'accounts' => true,
        'teams' => true,
    ],

    'depends' => [
        'blog' => ['auth'],
        'accounts' => ['auth'],
        'billing' => ['auth'],
        'teams' => ['auth'],
    ],
];

Everything defaults to on so the stack is fully functional out of the box; you disable what you don't want. auth is foundational — accounts, billing, teams, and blog all depend on it, so enabling any of them forces auth back on.

Runtime overrides via foundation_settings

Config values are only the defaults. The real, effective state is stored per-app in the foundation_settings table:

Column Type Notes
id bigint Primary key.
key string, unique Flag keys use the form features.{name} (e.g. features.billing).
value text, nullable '1' or '0'.
timestamps Created/updated.

Rows are managed through the Devdojo\Foundation\Models\FoundationSetting model (fillable: key, value). Because the store is a database table rather than a file, you can flip a feature for one deployment without editing config or redeploying — which is exactly how the platform toggles capabilities per app.

The Foundation API

Devdojo\Foundation\Foundation is the single source of truth for feature state. Its four static methods:

use Devdojo\Foundation\Foundation;

Foundation::features();                  // fully-resolved map<string,bool>
Foundation::enabled('billing');          // bool — the one you'll call most
Foundation::setFeature('billing', false); // persist an override to the DB

features() is where the resolution happens, in three passes:

  1. Start from the config('foundation.features') defaults, cast to booleans.
  2. Overlay any features.* rows from foundation_settings (only keys that exist in the defaults are honored).
  3. Run resolveDependencies() — walk every enabled feature and force-enable its prerequisites from the depends map.

enabled($feature) returns features()[$feature] ?? false. setFeature() writes a features.{feature} row via updateOrCreate.

Tip — Reading overrides is defensively guarded. storedOverrides() checks Schema::hasTable('foundation_settings'), uses the raw query builder (not Eloquent, so it works before the connection resolver is set), and wraps the read in a try/catch. If the database is unavailable — during install, discovery, or a fresh clone — it silently falls back to the config defaults.

How it's wired

The key detail is when the resolved flags are applied. In register(), the provider merges the config, then defers the override merge to an app booting callback:

$this->app->booting(function () {
    config()->set('foundation.features', Foundation::features());
});

This can't run in register() because the db service may not be bound yet. Booting callbacks fire at the very start of the boot phase — after the database is ready, but before any provider's boot(). So by the time each feature package reaches its own boot(), config('foundation.features') already reflects the database overrides and resolved dependencies.

Each feature package then gates itself on that config. For example, accounts opens its boot() with:

if (! config('foundation.features.accounts', true)) {
    return;
}

Note the , true default: every package treats "no Foundation installed" as "feature on," so each one still works standalone without the metapackage.

The /foundation/setup screen

The provider registers a view-foundation-setup middleware group and loads routes/foundation.php:

Method URI Name Purpose
GET foundation/setup foundation.setup Render the feature toggle UI.
POST foundation/setup foundation.setup.update Persist the submitted flags.
GET foundation Redirect to foundation/setup.

SetupController@index passes the resolved features() map and the depends config to the view. @update loops every configured feature and calls Foundation::setFeature() — with one guard: auth is always forced to true since it can't be disabled. Anything not present in the submitted features array is treated as off.

Access is protected by the ViewFoundationSetup middleware:

if (app()->isLocal() || Gate::allows('viewFoundationSetup')) {
    return $next($request);
}

abort(403);

So the screen is open in local, and everywhere else you authorize it by defining a viewFoundationSetup gate.

Warning — In production, define the viewFoundationSetup gate (e.g. restrict to admins). Without it, only local passes and every other environment returns 403 — including the environment where you actually want to manage flags.

Extension points

  • Gate features in your own code. Wrap platform behavior in Foundation::enabled('teams') rather than assuming a package is present, so a disabled feature disappears cleanly.
  • Toggle programmatically. Use Foundation::setFeature($name, $bool) from a job, seeder, or admin action to change flags without the setup UI.
  • Authorize the setup screen. Register a viewFoundationSetup gate to expose /foundation/setup to the right users in production.
  • Add prerequisites. Extend the depends map in the published config if you build features that require another package to be on.

Next, read the individual package pages — auth and accounts are the two packages most developers reach for first.

© 2026 DevDojo Edit this page