Building a SaaS app with Laravel Spark: Web Uptime

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.

  • JamesDiGioia

    What was your experience like building this while splitting the UI development between Blade and Vue.js? Was integrating them difficult? If I understood correctly, you did some pieces in Vue.js & some in Blade, correct? I would have to imagine the context-switching could be difficult.

    • It’s actually much nicer than you might think. Making use of Vue.js’s `inline-templates` makes it very nice to create the Vue.js UI components while still being able to use the power of blade (for things like blade includes for example).

  • jimmyrolando

    thank you for the post, while I’m raising money to purchase a license of Laravel Spark, i’m trying to integrate vuejs + laravel + vuex, can see de the code on https://github.com/jimmyrolando/laravel-vuejs-vuex I would appreciate your comments

  • doorsjm

    This has been extremely helpful as I was wondering where to start in regards to utilizing Vue.js because it was a bit confusing for me trying to put together how they built the current set of pages within Spark.

  • doorsjm

    Wouldn’t it be better to return an array from the all() function within the Controller with the count of the records in the database and the items based on the page number and amount per page instead of bringing back all the records from the database which will become bothersome when a large amount of records are being pulled in? In essence doing some of the pagination work there as opposed to the index() function.

    • In my quest for using Vue vs Blade, this is one of my primary questions. I’ve seen a lot of suggestions for using Vue over Blade so that you can take advantage of its many UI-related features, but at what cost if you have 1,000, 5,000, or more records you have to pass to the client?

      I personally have less than 400 records with 6 or 7 fields each, which I’m hoping won’t be too bad (haven’t tested yet), but I’m curious for when it might start getting bandwidth-heavy.

      • Michał

        I know this comment is a year old, but I’ll reply anyway :). If you’re passing 5k records to the client, it makes no difference if you inline them with blade or request them async from Vue. It’s still 5k records. Ideally, you either inline those that you need, or request a single page from Vue. When the user needs another page, you make another request.

        • Yea boy I was green in the JavaScript framework fields back then. After completing 2 Laravel/Blade projects that Dec, I ended up doing 3 months of Vue/vuex in Jan-Mar 2017, then 6 months of React, and I have since learned the way of the SPA. 🙂 Still good info for others though. Thanks.

          • Michał

            Yeah, I bet Keith :). I, myself, sometimes learn from new replies to older comments (or forum threads), so that’s def for others :). Cheers!

  • Joris Eversen

    Nice post, thanks ! It helped me a step forward in getting around Spark a little faster. Too bad the application seems to be offline; wanted to take a peek inside to see how you’ve done a few things 🙂

    Best regards,
    Joris