Building a SaaS app with Laravel Spark: Web Uptime

#
By Gilbert Pellegrom

If you keep up with the Laravel community at all you’ll be aware that Taylor Otwell recently released his much anticipated SaaS scaffolding library called Spark. Spark is a library built on top of Laravel and Laravel’s billing library Cashier that basically comes with all the parts of a SaaS app that you don’t want to spend time building. It includes:

  • Authentication and password reset
  • Subscription billing and invoices
  • Teams and team billing
  • Announcements, two-factor authentication, user impersonation

I could go on, suffice to say it covers all of the boring parts that most SaaS app’s require, and some of the most difficult parts to get right the first time, and saves hours of development. It’s not free (a basic license costs $99 at the moment) but if you count the time you’ll save building all of the above yourself, and the cost of all the bugs you’ll likely introduce building it yourself, I hope you’ll agree it’s well worth the money.

I’ve built and sold a few SaaS apps in the past so I was very excited to see how easy it was going to be to build an app using Spark. I started a new side project as a way to learn Spark and decided to build a website uptime monitoring app called Web Uptime.

In this article I’m going to look at developing an app using Spark and Spark’s front-end framework of choice: Vue.js.

Spark: Vue.js + Blade

If you haven’t already guessed, we’re fans of Vue.js here at Delicious Brains (both myself and Ian have written about it recently). So I’m not going to explain how Vue.js works in this article. Rather, I’m going to look at how it has been implemented in Spark and how it makes developing apps fast and simple. Note that Spark actually comes with a demo to-do app so you can see how Taylor has designed Spark to be used.

Having worked with Vue.js in the past, one of the first differences you notice with Spark is that every component has an inline-template. This means that you get the best of both worlds. You can create the templates for your components in a simple way, while still being able to use the power of Blade templates if you need it. As an example, below is the Spark Settings component:

@extends('spark::layouts.app')

@section('scripts')
   @if (Spark::billsUsingStripe())
       <script src="https://js.stripe.com/v2/"></script>
   @else
       <script src="https://js.braintreegateway.com/v2/braintree.js"></script>
   @endif
@endsection

@section('content')
<spark-settings :user="user" :teams="teams" inline-template>
   <div class="spark-screen container">
       <div class="row">
           <!-- Tabs -->
           <div class="col-md-4">
               <div class="panel panel-default panel-flush">
                   <div class="panel-heading">
                       Settings
                   </div>

                   <div class="panel-body">
                       <div class="spark-settings-tabs">
                           <ul class="nav spark-settings-stacked-tabs" role="tablist">
                               <!-- Profile Link -->
                               <li role="presentation">
                                   <a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">
                                       <i class="fa fa-fw fa-btn fa-edit"></i>Profile
                                   </a>
                               </li>

                               <!-- Teams Link -->
                               @if (Spark::usesTeams())
                                   <li role="presentation">
                                       <a href="#teams" aria-controls="teams" role="tab" data-toggle="tab">
                                           <i class="fa fa-fw fa-btn fa-users"></i>Teams
                                       </a>
                                   </li>
                               @endif

                               <!-- Security Link -->
                               <li role="presentation">
                                   <a href="#security" aria-controls="security" role="tab" data-toggle="tab">
                                       <i class="fa fa-fw fa-btn fa-lock"></i>Security
                                   </a>
                               </li>

                               <!-- API Link -->
                               @if (Spark::usesApi())
                                   <li role="presentation">
                                       <a href="#api" aria-controls="api" role="tab" data-toggle="tab">
                                           <i class="fa fa-fw fa-btn fa-cubes"></i>API
                                       </a>
                                   </li>
                               @endif
                           </ul>
                       </div>
                   </div>
               </div>

               <!-- Billing Tabs -->
               @if (Spark::canBillCustomers())
                   <div class="panel panel-default panel-flush">
                       <div class="panel-heading">
                           Billing
                       </div>

                       <div class="panel-body">
                           <div class="spark-settings-tabs">
                               <ul class="nav spark-settings-stacked-tabs" role="tablist">
                                   @if (Spark::hasPaidPlans())
                                       <!-- Subscription Link -->
                                       <li role="presentation">
                                           <a href="#subscription" aria-controls="subscription" role="tab" data-toggle="tab">
                                               <i class="fa fa-fw fa-btn fa-shopping-bag"></i>Subscription
                                           </a>
                                       </li>
                                   @endif

                                   <!-- Payment Method Link -->
                                   <li role="presentation">
                                       <a href="#payment-method" aria-controls="payment-method" role="tab" data-toggle="tab">
                                           <i class="fa fa-fw fa-btn fa-credit-card"></i>Payment Method
                                       </a>
                                   </li>

                                   <!-- Invoices Link -->
                                   <li role="presentation">
                                       <a href="#invoices" aria-controls="invoices" role="tab" data-toggle="tab">
                                           <i class="fa fa-fw fa-btn fa-history"></i>Invoices
                                       </a>
                                   </li>
                               </ul>
                           </div>
                       </div>
                   </div>
               @endif
           </div>

           <!-- Tab Panels -->
           <div class="col-md-8">
               <div class="tab-content">
                   <!-- Profile -->
                   <div role="tabpanel" class="tab-pane active" id="profile">
                       @include('spark::settings.profile')
                   </div>

                   <!-- Teams -->
                   @if (Spark::usesTeams())
                       <div role="tabpanel" class="tab-pane" id="teams">
                           @include('spark::settings.teams')
                       </div>
                   @endif

                   <!-- Security -->
                   <div role="tabpanel" class="tab-pane" id="security">
                       @include('spark::settings.security')
                   </div>

                   <!-- API -->
                   @if (Spark::usesApi())
                       <div role="tabpanel" class="tab-pane" id="api">
                           @include('spark::settings.api')
                       </div>
                   @endif

                   <!-- Billing Tab Panes -->
                   @if (Spark::canBillCustomers())
                       @if (Spark::hasPaidPlans())
                           <!-- Subscription -->
                           <div role="tabpanel" class="tab-pane" id="subscription">
                               <div v-if="user">
                                   @include('spark::settings.subscription')
                               </div>
                           </div>
                       @endif

                       <!-- Payment Method -->
                       <div role="tabpanel" class="tab-pane" id="payment-method">
                           <div v-if="user">
                               @include('spark::settings.payment-method')
                           </div>
                       </div>

                       <!-- Invoices -->
                       <div role="tabpanel" class="tab-pane" id="invoices">
                           @include('spark::settings.invoices')
                       </div>
                   @endif
               </div>
           </div>
       </div>
   </div>
</spark-settings>
@endsection

As you can see using Blade @includes and @if conditionals can be very helpful while maintaining the compatibility with Vue.js components. Purists will argue that doing conditionals like Spark::canBillCustomers() on the backend is not a separation of concerns and should be done purely in the front-end Vue.js components. However, I like this style of mix-n-match as it makes it simple and easy for developers to get up and running quickly, especially for developers coming from a Laravel (PHP) background that don’t feel comfortable using these fancy new JS frameworks yet.

Vue.js Components

Spark has lot’s of UI components already built for you (e.g. settings, billing etc.) so it’s actually pretty easy to see how to customise things and build your own components.

Taylor has been smart in the way that he’s built Spark’s components to make them easy to extend. All of Spark’s components live in /resources/assets/js/spark-components and basically contain mixins: [base] which means all of the main Spark components reside in the main spark folder but are included as mixins in the spark-components folder. The upshot of this is that you can easily extend the built-in components without worrying about updates to the base components whenever Spark needs an update.

To add your own components it’s as simple as creating the JS file and including it in the bootstrap.js file so it is globally available (Laravel’s Elixir does all the compiling and transpiling into a single JS file). As an example, below is the Sites component I built for Web Uptime:

Vue.component('sites', {
   props: ['user'],

   data() {
       return {
           sites: {
               data: [],
               current_page: 0,
               last_page: 0,
               per_page: 0,
               from: 0,
               to: 0,
               total: 0,
               next_page_url: null,
               prev_page_url: null
           },
           isLoadingSites: false,
           creatingSite: false,
           createSiteForm: new SparkForm({
               url: '',
           }),
           incidents: []
       }
   },

   created() {
       this.getSites();
       this.getIncidents();
   },

   methods: {
       getSites(page) {
           if (typeof page === 'undefined') {
               page = 1;
           }

           this.isLoadingSites = true;
           this.$http.get('/api/sites', { page: page })
               .then(response => {
                   this.sites = response.data;
                   this.isLoadingSites = false;
               });
       },
       prevPage() {
           this.getSites(--this.sites.current_page);
       },
       nextPage() {
           this.getSites(++this.sites.current_page);
       },
       selectPage(page) {
           this.getSites(page);
       },

       createSite() {
           this.creatingSite = true;
           this.createSiteForm.url    = '';

           $('#modal-create-site').modal('show');
           setTimeout(() => {
               $('#modal-create-site .on-focus').focus();
           }, 500);
       },
       store() {
           Spark.post('/api/sites', this.createSiteForm)
               .then(() => {
                   this.creatingSite = false;
                   this.getSites();

                   $('#modal-create-site').modal('hide');
               });
       },

       getIncidents() {
           this.$http.get('/api/incidents/open')
               .then(response => {
                   this.incidents = response.data;
               });
       }
   }
});

This is mostly just normal Vue.js component stuff but there are a few things worth mentioning here:

  • The sites data object has the format of a Laravel paginator object. This combined with the prevPage, nextPage and selectPage methods makes it simple to implement Laravel’s pagination on the front-end.
  • Spark comes with the vue-resource plugin included, which makes it easy to do this.$http.get() and this.$http.post() requests.
  • createSiteForm is actually a SparkForm object. This is a built-in helper to make it easy to handle form submission in Vue.js (e.g. Spark.post('/api/sites', this.createSiteForm)).

Back-end

Spark doesn’t really specify how to set up back-end controllers but that’s what Laravel is great at so it’s fairly simple to do what you normally do. However, all of the authentication is handled automatically by Spark. It generates “transient”, short-lived API tokens behind the scenes automatically when users load the application’s pages. This means you can use your own API without having to worry about authentication and credentials as it’s already handled automatically by Spark.

So you want to write an API in Laravel but you also want to keep your code DRY and extensible right? For Web Uptime I created an ApiController to make it easy to add new API endpoints for my different models with the minimum code possible:

This class makes use of Laravel’s Pagination, Validation and Authorization libraries (so you will have to set up your own Policies for this to work) but otherwise it’s just basic Laravel resource routing and Eloquent model methods.

Now every time you want to add a new model endpoint you can do something simple like this:

<?php

namespace App\Http\Controllers\Api;

use App\Site;
use Illuminate\Http\Request;

class SitesController extends ApiController
{

   protected function modelClass()
   {
       return Site::class;
   }

   protected function all(Request $request)
   {
       $sites = $request->user()->currentTeam->sites()->get();

       return $sites->sortByDesc('created_at')->all();
   }

   protected function validationRules()
   {
       return [
           'team_id' => 'required|integer',
           'url' => 'required|url',
       ];
   }
}

Just remember to add the resource route to your routes.php or api.php file:

Route::resource('sites', 'SitesController', ['except' => ['create', 'edit']]);

Over to you

Hopefully this article has provided some insight into just how simple Spark makes it to build full blown SaaS apps using Laravel and Vue.js, not to mention the time you save using the scaffolding that comes with Spark. To learn more about how to use Spark you should have a look at their docs.

Have you built an app using Spark? Would you be interested in using Spark to build an app? Let us know in the comments.

About the Author

Gilbert Pellegrom

Gilbert loves to build software. From jQuery scripts to WordPress plugins to full blown SaaS apps, Gilbert has been creating elegant software his whole career. Probably most famous for creating the Nivo Slider.