Building membership websites with Laravel and Statamic

Statamic Published on 4 min read
Statamic entry example with logged in protection.

Membership websites are a fun challenge. They’re usually content-heavy, so you want a CMS like Statamic. But you also need to manage members authentication, and the business logic. And thats where Laravel comes in.

The catch is that Statamic’s authentication forms store new users in flat files by default, next to Statamic users like admins and editors. That’s not ideal. Because if member users do not have access to the Statamic control panel, this creates confusion. Also, as the flat files grow to thousands, it will get messy and impact site performance.

I had a client project with this exact problem. It was an education site with gated content.

So I’ll show you, step by step, how I solved it. You’ll see how to gate content in Statamic and how to split responsibilities so editors publish in Statamic while Laravel tackles the member features.

1. Setup two authentication guards

To separate Statamic users from your Laravel app users, set up two user guards and two user providers in config/auth.php.

You need one guard and provider for Eloquent users and another for Statamic users:

// config/auth.php
 
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
 
'statamic' => [
'driver' => 'session',
'provider' => 'statamic',
],
],
 
'providers' => [
'statamic' => [
'driver' => 'statamic',
],
 
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
],
php

This gives each user group its own “door” and storage.

While you’re in config/auth.php, add separate brokers for each provider. You’ll have two for Statamic and one for Eloquent. Statamic splits resets and activations, while Eloquent doesn’t by default.

//config/auth.php
 
'passwords' => [
'resets' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
 
'statamic_resets' => [
'provider' => 'statamic',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
 
'statamic_activations' => [
'provider' => 'statamic',
'table' => env('AUTH_ACTIVATION_TOKEN_TABLE', 'password_activation_tokens'),
'expire' => 4320,
'throttle' => 60,
],
],
php

Now each user type can reset or activate accounts in the right place.

Next, in Statamic’s users.php config, choose which guard is used on the frontend and which is used for the Control Panel:

In the example below, we’re using the statamic driver for Control Panel users and the web driver for frontend users.

// config/statamic/users.php
 
'guards' => [
'cp' => 'statamic',
'web' => 'web',
],
php

This keeps editors in the CP guard and members in the app guard.

Any non-Statamic routes (for example, routes you’ve added in routes/web.php) are unaffected by these changes. Those routes use the guard set inside the defaults array in config/auth.php. In a fresh Statamic install it should be web.

Also, ensure passwords points to the correct reset and activation configs:

// config/statamic/users.php
 
'passwords' => [
'resets' => 'statamic_resets',
'activations' => 'statamic_activations',
],
php

This wires Statamic’s flows to the right broker entries.

After this setup we effectively start using the database with Statamic. So don’t forget to set up your local database and run:

php artisan migrate
bash

This creates the tables your auth flows need.

2. Authentication forms conundrum

The easiest way to build auth forms is to use Statamic user tags. The problem is that tags always talk to the user repository set in config/statamic/users.php. By default it is file.

Statamic does allow storing users in a database. That lets you use all user form tags without worrying about auth routes and controllers. However, we run into the same problem I mentioned earlier. Now all users, Statamic editors and app users, get lumped together. I think this is better than flat files, but it still may not fit your project.

If you go this route, you also get the bonus of using Statamic roles and permissions without a separate package, and you can manage app users through the CP. But if you plan to use the Filament admin panel for the app side, it's irrelevant.

So there are trade-offs. But for me, lumping all users together feels a bit icky.

So let’s set this up ourselves. Don’t worry. We won’t write it from scratch.

Using Laravel starter-kit

To set up auth ourselves, the quickest way is to create a fresh Laravel app with a starter-kit and copy what we need for auth. The starter-kit docs also explain guards and scaffolding links you can trace back.

Select the Vue starter-kit. This may help later if you want to set up Vue.js for your app and even turn it into an SPA with Inertia.

Here is what we need to copy:

  • Auth folder from app/Http/Controllers.

  • Auth folder from app/Http/Requests.

  • Auth folder from tests/Feature.

  • routes/auth.php and include it in routes/web.php:

    // routes/web.php
     
    require __DIR__.'/auth.php';
    php
  • Dashboard route in routes/web.php.

If you’re not going to use Inertia then update the create methods inside controllers to point to your views. Here is a register controller example:

<?php
 
namespace App\Http\Controllers\Auth;
 
//...
use App\Http\Controllers\Controller;
use Illuminate\View\View;
 
class RegisteredUserController extends Controller
{
/**
* Show the registration page.
*/
public function create(): View
{
return view('auth.register');
}
 
//...
}
php

This returns a Blade view instead of an Inertia response.

If you want to use Vue.js for auth, possibly with Inertia, and turn your app into an SPA, copy everything from resources/js and views/app.blade.php for your app routes. Laravel starter-kits come with TypeScript by default. You’ll need to integrate TypeScript into your project. Check package.json for required packages, and copy vite.config.ts, tsconfig.json, eslint.config.js.

Update the User model to enable email verification:

<?php
 
namespace App\Models;
 
use Illuminate\Contracts\Auth\MustVerifyEmail;
// ...
 
class User extends Authenticatable implements MustVerifyEmail
{
// ...
}
php

This makes Laravel send and enforce verification for users.

View templates

I’m not going to walk you through every view template here. Whether you use Livewire, Vue, or React, you can copy auth setup from the Laravel starter-kits. If you’re using Blade with Alpine, here is a community-maintained starter-kit from Laravel Daily.

In Bedrock, I use Blade with Alpine Precognition forms.

Some things to note about Bedrock setup:

  • You need to add middleware to all routes where you use a form with Precognition.

  • Precognition works with validation rules in the route’s form request. So refactor Controllers to use Form Requests. See form requests.

  • Update Controllers to return JsonResponse instead of redirect.

Check the final code for controllers and views in the Bedrock demo repository.

Maybe in the future I’ll update Bedrock with an option to scaffold authentication out of the box and let you pick Alpine.js with Blade or Vue.js with Inertia.

3. Gating Statamic content

You can protect your content per-entry or per-collection.

In Statamic they’re called Protectors.

Protected pages are automatically excluded from the static cache.

Protecting an entry

To protect an entry, add a protect variable with a corresponding scheme name.

Statamic comes pre-configured with a logged_in protection scheme. To see all schemes and configure your own, check config/statamic/protect.php.

Here is how your entry might look:

---
 
title: Members Only
 
protect: logged_in
 
---
 
When visiting this entry's URL, logged in users will see it,
 
but logged out users will be redirected to a login page.
yaml

This adds a per-entry gate with a simple “logged in” rule.

You can create a Button Group or Select field with a handle protect to let editors toggle this in the control panel instead of editing entries by hand.

Protecting a collection

To protect a whole collection, inject a protect variable to the collection’s YAML config file.

---
 
inject:
 
protect: logged_in
yaml

This applies the same rule to every entry in the collection.

Create a custom protection driver

Default logged_in protection does not enforce verified users. So let’s create a custom protector AuthVerified that extends the Authenticated protector.

// app/Protectors/AuthVerified.php
 
<?php
 
namespace App\Protectors;
 
use Illuminate\Support\Facades\Auth;
use Statamic\Auth\Protect\Protectors\Authenticated;
use Illuminate\Contracts\Auth\MustVerifyEmail;
 
class AuthVerified extends Authenticated
{
public function protect()
{
if (Auth::guest()) {
abort(redirect($this->getLoginUrl()));
}
 
$user = Auth::user();
 
if ($user instanceof MustVerifyEmail && !$user->hasVerifiedEmail()) {
abort(redirect(route('verification.notice')));
}
}
}
php

This blocks unverified users, and it redirects them to the right pages.

Register the driver inside a service provider’s boot method as per docs:

// app/Providers/AppServiceProvider.php
 
use App\Protectors\AuthVerified;
use Statamic\Auth\Protect\ProtectorManager;
 
public function boot(): void
{
app(ProtectorManager::class)->extend('auth_verified', function ($app) {
return new AuthVerified();
});
}
php

This makes your new driver available as auth_verified.

Add to the protect.php config file:

// config/statamic/protect.php
 
'schemes' => [
//...
 
'logged_in' => [
'driver' => 'auth',
'login_url' => '/login',
'append_redirect' => true,
],
 
'logged_in_verified' => [
'driver' => 'auth_verified',
'login_url' => '/login',
'append_redirect' => true,
],
 
//...
]
php

This gives you two choices: logged-in only, or logged-in and verified.

Where to take this next

You now have clean separation between Statamic editors and app members, plus flexible content gates.

If you tried a different approach or found pitfalls I should mention, message me on X. Or if you want me to add auth scaffolding option to Bedrock, let me know that too.

And if you’re building an educational platform, a community, or want to publish members-only content, and looking for help, I’m available for hire.

Jason Baciulis's avatar

Jason Baciulis

I’m a Laravel/Statamic dev and certified Statamic partner. I share practical tips from client work and Bedrock starter-kit. If this helped, say hi on X. Need a hand with Laravel or Statamic? Hire me.