Creating a Subscription-Based Website with Laravel and Recurly, Part 1

Lukas White
Lukas White

Scheduling and processing payments is easy, but don’t think recurring payments are trivial. Things can get complicated very quickly. You need to decide how to handle failures (how many failed payment attempts does someone get?), billing details must be kept up to date, and upgrading and downgrading plans can introduce all sorts of issues. Then there’s perhaps the most significant issue with recurring payments – you need to keep your visitors’ payment details on file. This introduces security, compliance, and legal issues. Luckily, Recurly is a service that handles most of the issues around taking payments, as well as processing recurring payments from there on out. Using its JavaScript libraries, you can create secure forms for submitting and updating billing information with Recurly – including the all-important credit card details – which submit the information securely, independent from your application. Once you’ve set up the amount and frequency of payments, the service takes care of scheduling and taking payments at regular intervals until the customer decides to cancel. Recurly also handles plan changes, calculating and making additional charges or refunds. In this two-part series I’ll show you step-by-step how to create a paid subscription-based membership website using Laravel, a PHP5-based framework, and the Recurly payment processing service. First we’ll start by creating a simple site with basic registration, authentication, and user roles and permissions. Then we’ll add payment processing and tie it into the registration process, allowing people to purchase different membership tiers.

Setting Up the Application

Start by creating a new project folder and run the following command:
composer create-project laravel/laravel recurly --prefer-dist
This creates a new Laravel project using Composer, which downloads the framework and its dependencies. We also need some additional libraries for later, so add the following in the require section of composer.json and run composer.phar update.
"machuga/authority": "dev-develop",
"machuga/authority-l4" : "dev-master",
"recurly/recurly-client": "2.1.*@dev"
This downloads the Authority library, which we’ll use for user roles and permissions, and the Recurly client library. Next, configure the database connection by specifying the appropriate schema name, username, password and hostname/address in app/config/database.php. Laravel works out-of-the-box with a number of databases, such as MySQL, Postgres, SQLite, and SQL Server. We also need a users table. Rather than set that up by hand, Laravel provides migrations, a means to create and modify database tables programmatically. Run the following command:
php artisan migrate:make create_users_table
This creates the migration file in the app/database/migrations folder. The file is created with a basic outline, but let’s flesh it out with our table definition:
public function up()
    Schema::create('users', function($table) {

public function down()
Go back to the command line and run:
php artisan migrate
Look in the database and you should find that Laravel has created the users table for you.

Setting up Authority for Roles and Permissions

To determine the type of account a user has and privileges it grants them, we’ll assign users to a role. We’ve already downloaded Authority via Composer to help with this; we just need to carry out a few more steps to configure it fully. In app/config/app.php, add the following line to the providers:
Add the following to the aliases:
'Authority' => 'Authority\AuthorityL4\Facades\Authority',
And publish the Authority configuration file:
php artisan config:publish machuga/authority-l4
We’ll return to the configuration file later. For now, we need to create some additional database tables. Fortunately, the package contains its own migrations for this. Run them with the following command:
php artisan migrate --package="machuga/authority-l4"
You’ll find you have three additional tables: permissions, roles and role_user. We also need to create models to represent roles and permissions. We’ll keep them simple for now. In app/models/Role.php
class Role extends Eloquent
And in app/models/Permission.php:
class Permission extends Eloquent 
Now we need to modify the User model class – which has already been created – to associate a user with roles and permissions. Add the following lines to app/models/User.php:
public function roles() {
    return $this->belongsToMany('Role');

public function permissions() {
    return $this->hasMany('Permission');

public function hasRole($key) {
    foreach($this->roles as $role){
        if ($role->name === $key) {
            return true;
    return false;
Now let’s pre-populate the database with some data. Open app/database/seeds/DatabaseSeeder.php and paste in the following:
class DatabaseSeeder extends Seeder
    public function run() {
        $this->command->info('User table seeded!');
        $this->command->info('Role table seeded!');

class UserTableSeeder extends Seeder
    public function run() {
            'email'    => '',
            'name'     => 'Joe Bloggs',
            'password' => Hash::make('password')


class RoleTableSeeder extends Seeder
    public function run() {
        Role::create(array('name' => 'admin'));
        Role::create(array('name' => 'pending'));
        Role::create(array('name' => 'member'));
        Role::create(array('name' => 'bronze'));
        Role::create(array('name' => 'silver'));
        Role::create(array('name' => 'gold'));
Then seed the database by running:
php artisan db:seed

Creating a Layout

Now we’ll create the overall page layout. Download Twitter Bootstrap and put the source files in the public
folder – moving the js files to public/js/libs. Create the file app/views/layouts/default.blade.php with the following contents:
<!DOCTYPE html>
<html lang="en">
  <meta charset="utf-8">
  <title>Subscription Site Tutorial</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="">
  <meta name="author" content="">

  <!-- Le styles -->
  <link href="/css/bootstrap.css" rel="stylesheet">  
  <link href="/css/style.css" rel="stylesheet"> 

  <div class="navbar navbar-inverse navbar-fixed-top">
   <div class="navbar-inner">
    <div class="container">
     <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
      <span class="icon-bar"></span>
     <a class="brand" href="#">Subscription Site Tutorial</a>
     <div class="nav-collapse collapse">
      <ul class="nav">
       <li class="active"><a href="#">Home</a></li>
       <li><a href="#about">About</a></li>
       <li><a href="#contact">Contact</a></li>
     </div><!--/.nav-collapse -->

  <div class="container">

    <div class="alert alert-success">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      {{ Session::get('success') }}

    <div class="alert alert-error">
      <button type="button" class="close" data-dismiss="alert">&times;</button>
      {{ Session::get('error') }}


  </div> <!-- /container -->

  <!-- Le javascript
  ================================================== -->
  <!-- Placed at the end of the document so the pages load faster -->
  <script src=""></script>
  <script src="/js/libs/bootstrap.min.js"></script>
It’s pretty basic stuff – our page content is outputted in the line that reads @yield('content')
. Next, create the file public/css/style.css:
body {
    padding-top: 50px;

p.logged-in {
    color: white;
    margin-top: 0.5em;
And finally, let’s create a basic home page for the application. Create the file app/views/home/index.blade.php:
  <h1>Subscription Site Tutorial</h1>
The @extends
declaration tells Laravel to use the default layout we just created with the content we’re injecting into the content area via the @yield command wrapped in the @section declaration. Don’t forget to change the default route accordingly in app/routes.php:
Route::get('/', function() {
	return View::make('home/index');

Building the Login Mechanism

We’ve got our users – and an account to start off with – so now we need the ability to log in. In app/routes.php, add a route to the login page:
Route::get('/auth/login', function() {
    return View::make('auth/login');
Now create the view, app/views/auth/login.blade.php. The .blade.php extension indicates that we’re going to use the Blade templating library which ships with Laravel, which is cleaner than straight PHP. Behind the scenes, these template files are compiled into PHP as required.
  <h1>Please Log in</h1>
  {{ Form::open(array('url' => 'auth/login')) }}
  {{ Form::label('email', 'E-Mail Address') }}
  {{ Form::text('email') }}
  {{ Form::label('password', 'Password') }}
  {{ Form::password('password') }}
  <div class="form-actions">
  {{ Form::submit('Login', array('class' => 'btn btn-primary')) }}
  {{ Form::close() }}
  <p>Not a member?  <a href="/user/register">Register here</a>.</p>
If you browse to /auth/login in your browser you should see a simple login form. To process the login, we’ll need to build a POST route. Authentication in Laravel is a breeze; we simply do this:
Route::post('/auth/login', function() {
    $email = Input::get('email');
    $password = Input::get('password');

    if (Auth::attempt(array('email' => $email, 'password' => $password))) {
        return Redirect::to('/')->with('success', 'You have been logged in');
    else {
        return Redirect::to('auth/login')->with('error', 'Login Failed');

    return View::make('auth/login');
All the magic happens in Auth::attempt(); if the login is successful, a session is created and a populated instance of the User object is accessible via the static method Auth::user()
. The logout method is equally straightforward:
Route::get('/auth/logout', function() {
    return Redirect::to('/')->with('success', 'You have successfully logged out');

Basic Registration

Our last task in this part is to set up a basic registration process. Create a registration route in app/routes.php:
Route::get('/user/register', function() {
    return View::make('user/register/index');
Now create the view app/views/user/register/index.blade.php
  {{ Form::open(array('url' => 'user/register')) }}
  {{ Form::label('email', 'E-Mail Address') }}
  {{ Form::text('email') }}
  {{ $errors->first('email') }}

  {{ Form::label('name', 'Your name') }}
  {{ Form::text('name') }}
  {{ $errors->first('name') }}

  {{ Form::label('password', 'Password') }}
  {{ Form::password('password') }}
  {{ $errors->first('password') }}

  {{ Form::label('password_confirmation', 'Repeat') }}
  {{ Form::password('password_confirmation') }}

  <div class="form-actions">
  {{ Form::submit('Register', array('class' => 'btn btn-primary')) }}
  {{ Form::close() }}
There are a couple of things to note here:
  • The $errors object is automatically passed to the view, regardless of whether there are any errors in the submission. If the field’s okay, it’ll just print nothing.
  • By appending _confirmation to the name of a field, it’s simple to validate the two fields together; i.e. confirm that the user has re-entered the chosen password correctly.
Now let’s implement the POST action:
Route::post('/user/register', function() {
    $validator = Validator::make(
            'name' => array('required', 'min:5'),
            'email' => array('required', 'email', 'unique:users'),
            'password' => array('required', 'confirmed')

    if ($validator->passes()) {
        $user = new User();
        $user->name     = Input::get('name');
        $user->email    = Input::get('email');
        $user->password = Hash::make(Input::get('password'));

        $role_pending = Role::where('name', '=', 'pending')->first();

        return Redirect::to('/')->with(
            'Welcome to the site, . Auth::user()->name . '!'
    else {
        return Redirect::to('user/register')->with(
            'Please correct the following errors:'
This is all fairly basic stuff – we create a validator, passing it the POST variables with Input::all() and a set of validation rules. If validation passes, we create a new user, assign them to the pending role, log them in, and then redirect them to the front page. If validation fails, we redirect back to the form, create an error flash message, and pass the error messages from the validator – they’ll then be available in the view in the $errors


In this part we’ve gone step-by-step through building the bare bones of a subscription site. It has the ability to register a basic account, login, and logout. In the next part we’ll integrate Recurly for paid subscription plans.

Frequently Asked Questions (FAQs) about Creating a Subscription-Based Website with Laravel and Recurly

What are the key differences between Laravel and other platforms for creating a subscription-based website?

Laravel is a PHP framework that offers a clean and elegant syntax, making it a joy to work with. It’s designed for developers who need a simple and elegant toolkit to create full-featured web applications. Laravel is different from other platforms because it comes with its own templating engine called Blade, which makes it easier to incorporate HTML and PHP in your views. It also has a robust ecosystem with tools like Laravel Mix for compiling assets and Laravel Echo for real-time broadcasting.

How does Recurly integrate with Laravel for subscription management?

Recurly is a subscription management platform that provides flexible subscription plans and pricing, automated communications, and advanced analytics. When integrated with Laravel, Recurly provides a seamless subscription management experience. Laravel communicates with Recurly’s API to manage subscriptions, process payments, and handle customer data.

Can I use other payment gateways with Laravel and Recurly?

Yes, Recurly supports over a dozen payment gateways, including PayPal, Stripe, and Braintree. You can choose the one that best suits your business needs. Laravel’s Cashier package also provides an expressive, fluent interface to various subscription billing services.

How secure is a Laravel and Recurly subscription website?

Laravel and Recurly both prioritize security. Laravel uses hashed and salted passwords, meaning your user’s passwords will be encrypted in the database. Recurly is PCI-DSS Level 1 compliant, the highest level of security a business can offer when it comes to protecting customers’ payment information.

How customizable is a Laravel and Recurly subscription website?

Laravel and Recurly offer a high level of customization. Laravel’s MVC architecture allows you to structure your website as you see fit, while Recurly’s flexible subscription plans and add-ons allow you to tailor your pricing and offerings to your business needs.

How scalable is a Laravel and Recurly subscription website?

Laravel and Recurly are both highly scalable. Laravel can handle large applications with complex back-end requirements, while Recurly can manage any number of subscribers and handle high volumes of transactions.

What kind of support is available for Laravel and Recurly?

Both Laravel and Recurly have strong communities and extensive documentation. Laravel has a dedicated forum and IRC channel, while Recurly offers support via email and live chat.

How does Laravel handle subscription renewals and cancellations?

Laravel’s Cashier package includes methods for handling subscription renewals and cancellations. You can easily check if a subscription is active, on trial, or cancelled, and handle these events accordingly.

Can I offer trial periods with a Laravel and Recurly subscription website?

Yes, both Laravel and Recurly support trial periods. You can set the length of the trial period when creating a subscription plan in Recurly, and Laravel’s Cashier package provides methods for handling trial periods.

How do I handle failed payments with Laravel and Recurly?

Recurly has a robust dunning management system for handling failed payments. Laravel’s Cashier package also includes methods for handling failed payments, allowing you to easily manage these situations.