Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

3KyNoX's avatar

Laravel Spark with Nova make sense?

Hello there,

I'm just discovering new Laravel Nova and looking to integrate it in Spark / or replace current Spark admin with Nova. What's your thoughts about this? Does it can be designed like this?

Thanks in advance!

0 likes
19 replies
Swaz's avatar

Nova doesn't know or care about Spark. They are very different things, you can use both or just one, it doesn't matter :)

jlrdw's avatar

Also, I know Taylor put a lot of work into Nova. But you do realize that an admin area is mostly links to somewhere with css padding, right. And other items can be simply be put there by coding yourself and using a div.

I know it's nice looking and all that, but ask yourself do you really need it if you are already pretty advanced at laravel.

Just my thoughts on it. No reply needed.

To add, I love laravel and how flexible Taylor designed it.

3KyNoX's avatar

Thanks for answers!

It could fit for my use case. That's why I'm asking.

I'm building a Spark Saas where I will give my customers ability to control their online service through an admin panel I have to develop.

Untill now, I planned to do it using Spark admin, also getting benefits of team billing and payment gateways available in Spark.

But, behind the scene, Spark uses Laravel, which is the case also for Nova. Now I'm discovering Nova, I'm asking myself which "product" is best to craft the admin panel for my users, if (what I think) Nova and Spark can dialog together, because built on the same base (Laravel).

Thanks again for your thoughts!

jlrdw's avatar

Jeffery will have lessons soon. Maybe he'll explain integration.

Deftly's avatar

This is my thought too.

We are using Spark for our SaaS for billing, teams, kiosk etc, but the would love to use the Nova Vue components for building out the rest of the interface.

CathalG's avatar

Any update on this one guys? I've created a fresh Spark project and installed Nova, I'd like to use most Nova's functionality but allow Spark to take care of user management and authentication.

zhorton999's avatar

@3kynox I actually did this recently for an Affiliate Marketing Platform.

I wouldn't recommend it unless you are very competent in Vue, but if you are okay troubleshooting a few compatibility bugs here and there then it's an AMAZING combination.

Most compatibility bugs are really simple to correct, but some are definitely funky.

That being said, the combination of the two has saved my team a TON of time.

That being said there are usually only 2 of us, with an occasional 3rd developer pitching in for tracking integration.

I'm not sure I would recommend this for a larger team until the initial configuration is set up and the hitches are figured out.

Here's the code for the updated nova/resources/views/layout.blade.php to get the initial configuration working properly. (There is some extra code that you'll need to remove, such as the reference to config)


<!DOCTYPE html>
<html lang="en" class="h-full font-sans antialiased">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=1280">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>Spark Nova</title>

    <link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,800,800i,900,900i" rel="stylesheet">
    <link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
    <link href="{{ mix(Spark::usesRightToLeftTheme() ? 'css/app-rtl.css' : 'css/app.css') }}" rel="stylesheet">
    <link rel="stylesheet" href="{{ mix('app.css', 'vendor/nova') }}">

    @foreach(Nova::availableStyles(request()) as $name => $path)
            <link rel="stylesheet" href="/nova-api/styles/{{ $name }}">
    @endforeach

    <!-- Scripts -->
    @stack('scripts')

    <!-- Global Spark Object -->
    <script>
        window.Spark = @json(array_merge(Spark::scriptVariables(), [
            'afill' => config('afill'),
        ]));
    </script>
</head>
<body>
    <div id="spark-app" v-cloak>


        <!-- Navigation -->
        @if (Auth::check())
            @include('spark::nav.user')
        @else
            @include('spark::nav.guest')
        @endif


        <!-- Application Level Modals -->
        @if (Auth::check())
            @include('spark::modals.notifications')
            @include('spark::modals.support')
            @include('spark::modals.session-expired')
        @endif


    </div>


    <div id="nova" class="container-fluid">
        <br>
        <global-search></global-search>
        <div v-cloak class="row">
            <!-- Content -->
            <div data-testid="content" class="col-md-10 offset-md-1">
                @yield('content')

                @include('nova::partials.footer')
            </div>

        </div>
    </div>

    <script>
        window.config = @json(Nova::jsonVariables(request()));
    </script>

    <!-- Scripts -->
    <script src="{{ mix('manifest.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('vendor.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('app.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('js/app.js') }}"></script>
    <script src="/js/sweetalert.min.js"></script>

    <script>
        window.Nova = new CreateNova(config)
    </script>

    <script src="/nova-api/scripts/nova"></script>
    @foreach (Nova::availableScripts(request()) as $name => $path)
        @if (starts_with($path, ['http://', 'https://']))
            <script src="{!! $path !!}"></script>
        @else
            <script src="/nova-api/scripts/{{ $name }}"></script>
        @endif
    @endforeach

    <script>
        Nova.liftOff()
    </script>


</body>
</html>

There are also some css conflicts that you'll have to fix when you combine the two.

Example: The Nova actions modal doesn't display when initially combined.

I created a resources/scss/nova directory and broke down the scss fixes into individual files.

One of those files is actions.scssand corrects the actions not displaying correctly.

.w-action {
  margin-top: 100px;
}
.w-action-fields {
  width: 850px !important;
}
.relative.mx-auto.flex.justify-center.z-20.py-view
{
  top: 20vh;
}

.action-button {
  width: 200px;
}

.action-button > button {
  width: 200px;
}

.action-button.index {
  align-self:center;
  margin-bottom: 4px;
  margin-left: 3px;
}

.action-button.detail {
  margin-left: auto;
}

Another funky bug when combining Nova and Spark was getting the Nova resource links to display and work properly outside of the Nova Vue instance (EX: inside of the Spark Nav Bar).

To solve that problem I:

  1. Created a Vue component nova-link.vue (Registered to the Spark Instance)
  2. Iterated Through The Nova Links I wanted to display.
  3. Used the window.Nova.$router instance to interact with the Nova Vue router (see the go() method)
<template>
    <li class="nav-item" @click="go" v-if="display">
        <a class="nav-link">
            <slot>label</slot>
        </a>
    </li>
</template>

<script>
    export default {
        props: ['to'],

        created()
        {
            this.dynamicallyHideLinks()

            this.nova.links[this.to.params.resourceName] = this.to
        },
        computed: {
            nova() {
                return Spark.afill['nova']
            },
            display() {
                return (this.nova.hidden.includes(this.to.params.resourceName)) ? false : true
            }
        },
        methods: {
            dynamicallyHideLinks()
            {

                let hideResource = (Spark.state.currentTeam.type.matches('Advertiser'))
                    ? 'campaigns'
                    : 'opportunities'

                if(this.nova.hidden.includes(hideResource)) return

                this.nova.hidden.push(hideResource)
            },
            go() {
                window.Nova.app.$router.push(this.to)
            }
        }
    }
</script>

Notice the go() method has to access the Nova vue instance globally. No big problem as Nova comes set as a global window instance, but if you're not extremely familiar with Vue & the Vue router than this can be extremely frustrating.

The last weird thing with navigation (Honestly the most difficult part of compatibility fixes) was getting the Nova Resource links to work properly when on non-nova pages. I set my Nova root uri to /home.

On non Nova pages, the Nova router/instance are never rendered ~ so you can't even use the Nova window instance to access the Nova Vue router or Nova Vue Instance.

<!-- Only Renders On Non Nova Pages -->
@if($notANovaPage))
    @foreach(config('afill.nova.resources') as $resource)
        @if(!in_array($resource, config('afill.nova.hidden')))
            <li class="nav-item">
                <a class="nav-link" href="{{ $resource->path }}">
                    {{ $resource->name }}
                </a>
            </li>
        @endif
    @endforeach
@endif

<!-- Only Renders On Nova Pages -->
@foreach (Nova::availableTools(request()) as $tool)
    {!! $tool->renderNavigation() !!}
@endforeach

I'm sure there are several workarounds to this bug, I just haven't gotten to it yet. In fact, one workaround would be to set your Nova root uri to / and then Nova would be rendered on every page ~ thus you would always have access to the Global window.Nova.app instance ~ Lookit that, learning more as we go :)

In closing, combining Nova and Spark has been one of the best decisions I've made in my software career :)

I am in absolute love with the entire set up. It is an INCREDIBLY POWERFUL combination ---- if you have a very thorough understanding of Vue, are comfortable working with two Vue Instances, and referencing those instances through their given global references.

Spark Vue Instance:window.app Nova Vue Instance: window.Nova.app

If you have any more questions or decide to move forward feel free to shoot any questions you may have my way :)

All the best, Zachary Horton Clean Code Studio ~ Simplify!

6 likes
skalero01's avatar

I could mix both services without modifying any JS. Have the next code, hope it helps somebody

<!DOCTYPE html>
<html lang="en" class="h-full font-sans antialiased">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=1280">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ Nova::name() }}</title>

    <link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,800,800i,900,900i" rel="stylesheet">
    <link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
    <link rel="stylesheet" href="{{ mix('app.css', 'vendor/nova') }}">
    <link href="{{ mix('css/spark-header.css') }}" rel="stylesheet">
    
    <link rel="icon" href="/favicon.png" />

    @foreach(Nova::availableStyles(request()) as $name => $path)
            <link rel="stylesheet" href="/nova-api/styles/{{ $name }}">
    @endforeach

    <!-- Scripts -->
    @stack('scripts')

    <!-- Global Spark Object -->
    <script>
        window.Spark = @json(array_merge(Spark::scriptVariables(), []));
    </script>
</head>
<body class="min-w-site bg-40 text-black min-h-full">
    <div id="spark-app" v-cloak>
        <!-- Navigation -->
        @if (Auth::check())
            @include('spark::nav.user')
        @else
            @include('spark::nav.guest')
        @endif

        <!-- Application Level Modals -->
        @if (Auth::check())
            @include('spark::modals.notifications')
            @include('spark::modals.support')
            @include('spark::modals.session-expired')
        @endif
    </div>
    <div id="nova">
        <div v-cloak class="flex min-h-screen">
            <!-- Sidebar -->
            <div class="min-h-screen flex-none pt-header min-h-screen w-sidebar bg-grad-sidebar px-6">
                @foreach (Nova::availableTools(request()) as $tool)
                    {!! $tool->renderNavigation() !!}
                @endforeach
            </div>

            <!-- Content -->
            <div class="content">
                <div class="flex items-center relative shadow h-header bg-white z-20 px-6">
                    <a v-if="'{{ Nova::name() }}'" href="{{ Config::get('nova.url') }}" class="no-underline dim font-bold text-90 mr-6">
                        {{ Nova::name() }}
                    </a>

                    @if (count(Nova::globallySearchableResources(request())) > 0)
                        <global-search></global-search>
                    @endif
                </div>

                <div data-testid="content" class="px-view py-view mx-auto">
                    @yield('content')

                    @include('nova::partials.footer')
                </div>
            </div>
        </div>
    </div>

    <script>
        window.config = @json(Nova::jsonVariables(request()));
    </script>

    <!-- Scripts -->
    <script src="{{ mix('manifest.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('vendor.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('app.js', 'vendor/nova') }}"></script>
    <script src="{{ mix('js/app.js') }}"></script>
    <script src="/js/sweetalert.min.js"></script>

    <script>
        window.Nova = new CreateNova(config)
    </script>

    @foreach (Nova::availableScripts(request()) as $name => $path)
        @if (starts_with($path, ['http://', 'https://']))
            <script src="{!! $path !!}"></script>
        @else
            <script src="/nova-api/scripts/{{ $name }}"></script>
        @endif
    @endforeach

    <script>
        Nova.liftOff()
    </script>


</body>
</html>

And created an spark-header.scss file

#spark-app {
    @import "./../../vendor/laravel/spark-aurelius/resources/assets/sass/variables";
    @import '~bootstrap/scss/bootstrap';
    @import '~sweetalert/dist/sweetalert.css';
    @import "./../../vendor/laravel/spark-aurelius/resources/assets/sass/components/dropdown";
    @import "./../../vendor/laravel/spark-aurelius/resources/assets/sass/components/navbar";
    @import "./../../vendor/laravel/spark-aurelius/resources/assets/sass/components/notifications"; 
    @import './../../vendor/laravel/spark-aurelius/resources/assets/sass/elements/utilities';

    // Vue Cloak
    [v-cloak] {
      display: none;
    }

    body {
      direction: ltr;
    }

    .profile-photo-preview {
      display: inline-block;
      background-position: center;
      background-size: cover;
      vertical-align: middle;
      height: 60px;
      width: 60px;
      border-radius: 50%;
    }

    pre {
      display: block;
      padding: 10.5px;
      margin: 0 0 11px;
      font-size: 13px;
      line-height: 1.6;
      word-break: break-all;
      word-wrap: break-word;
      color: #333333;
      background-color: #f5f5f5;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .plan-feature-list{
      list-style: none;
    }

    .plan-feature-list li {
      line-height: 25px;
      margin-bottom: 20px;

      &:last-child{
        margin-bottom: 0;
      }
    }

    .spark-profile-photo-xs {
      border-radius: 50%;
      height: 20px;
      width: 20px;
    }

    .spark-profile-photo {
      height: 35px;
      width: 35px;
      border-radius: 50%;
    }

    .spark-profile-photo-xl {
      height: 125px;
      width: 125px;
      border-radius: 50%;
    }
    
}
.pt-header {
    padding-top: 2rem !important;
}
.h-header  {
    text-align: center;
    background-color: #f9f9f9 !important;
    border-top: 1px solid #ddd;
    justify-content: center;
}
.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1040;
  width: 100vw;
  height: 100vh;
  background-color: #373f43;
}

.modal-backdrop.fade {
  opacity: 0;
}

.modal-backdrop.show {
  opacity: 0.5;
}
wmfairuz's avatar

@ZHORTON999 - Thanks. You should make a blog post on this topic. There's not much resources on combining these two.

3KyNoX's avatar

Thanks a lot for all that stuff fellowz!

I'm about to restart more laravel/vue learn and development until now (as I was a lot busy these past months).

I'm okay to put all of this in practice soon (as well ideas from this topic https://laracasts.com/discuss/channels/spark/spark-for-a-spa-start-from-scratch-or-build-on-whats-there?page=1#reply=507710

I'm okay to write a blog post and give credits of anyone that produce the ways of managing this particular case here. The use case will be a full backend (Spark) + frontend (VueJs/Quasar) website and an admin panel (Nova).

What will be covered are:

  1. Merging Laravel Spark & Nova (no need of two laravel installations to manage a single product)
  2. Transform Spark to a full API (no more blade pages except for specific needs)
  3. Maybe transform as well Nova to a full API
  4. Reproduce Spark pages using Quasar Vue Framework (maybe Nova ones also)
  5. Keep stuff maintainable (for future updates)

That way, Spark & Nova become real Single Page Application, that can be used natively (using Quasar that can produce win/mac/linux/android/ios/pwa builds).

Because Spark & Nova are closed source, only a blog will be shown that explain all the steps.

2 likes
mibou520's avatar

@3KYNOX - Thanks for your initiative.

I'll suggest integrating Spark functionalities inside Nova since I think that managing teams, users, billing, invitations is generally done by the Administrator/Admin Panel.

Exemple, having a Team resource on Nova side to manage Team members, subscriptions, invitations... My concern is how to reproduce Spark's functionalities inside Nova and trigger the same events as Spark does interacting with Spark UI.

3KyNoX's avatar

@mibou520 It might not be the case if we consider there's an admin panel for users (managed in Spark) and another one for the real administrative functions on top of entities (Nova).

mibou520's avatar

@3KYNOX - Yeah I get your point! ;) If you don't want to give your users access to Nova.

On my side, I'm considering using Nova as the Admin Panel where Users with Administrative roles can manage their accounts. Otherwise, I'll have to re-create Nova's engineering inside my Spark application and I'm still in the MVP (Minimum Viable Product) stage with minimum resources (Only myself) working on it.

Can't wait to see your blog post.

foishii's avatar

I've decided to move away from Spark after researching and playing around with the combination of using Nova in a Spark project.

I'm migrating to Laravel + Cashier + Nova, based on my project's requirements and my resources. I'll have to develop some custom interfaces but I'm hoping it'll be a more flexible approach for the long run.

There's also this Nova Cashier Overview Nova Package that I may end up using.

Please or to participate in this conversation.