404 When Viewing Invoices as Client

Version 5.12.18-W174 self-hosted

Environment Shared hosting but I have root access

Checklist

  • Can you replicate the issue on our v5 demo site https://demo.invoiceninja.com or Invoice Ninja? It’s self hosted, not sure this applies
  • Have you searched existing issues? Yes
  • Have you inspected the logs in storage/logs/laravel.log for any errors? Yes, there are no errors

Describe the bug

Clients receive a 404 error when they go to view invoices/quotes/etc via the web. I know this has to do with Livewire as that is where I’m getting the 404 when I inspect the page and view the network tab. I have tried the Livewire nginx and Apache fix to no avail.

Steps To Reproduce

View an invoice as a customer and instead of seeing the invoice you see a darkened screen with a 404 error.

Expected Behavior

See the invoice

Additional context

Site is running in WHM/cPanel. PHP version 8.3 on Apache with NGINX caching via NGINX Manager.

InvoiceNinja is installed in a subfolder of the webroot folder.
APP_URL=“https://mydomain.xyz/inv
Path on disk: /home/mydomain/public_html/inv

As I said, I know this is a Livewire issue but the workaround from: Fix the /livewire/livewire.js 404 not found error (2025) isn’t working for me and I’m not sure where to go from here. I assume it’s because my IN installation is in a subfolder because the Livewire URL is attempting to access https://mydomain.xyz/livewire/update

I have been through every forum on here and tried everything I have found with the exception of backing up and reinstalling and using a subdomain which was suggested here: Can't view any invoice in client portal - Livewire is not defined console error - #23 by SteveMc

I’m trying to avoid reinstallation into a subdomain but if that’s the only way to make this work then I’ll give it a shot… or maybe it’s time to move away from self-hosted.

Client portal:

Any help is greatly appreciated

Hi,

Are you able to map your web server web toor to the /public folder?

Here’s what ChatGPT suggests in case it’s helpful:

You’re right: this is Livewire being served from the wrong base path when the app lives in a subfolder. Livewire is trying to hit /livewire/update at the domain root, but your install is at /inv, so the endpoint should be /inv/livewire/update (and the script should be /inv/livewire/livewire.js). On cPanel/Apache with the NGINX Manager in front, you also need to make sure NGINX doesn’t “short-circuit” those dynamic routes.

Here’s a tight, subfolder-safe fix that works for Livewire v3 (and also lists v2 alternatives), plus the cPanel NGINX bit.


1) Make Laravel/Livewire aware of the subfolder

A. Confirm the basics

  • In .env:

    APP_URL="https://mydomain.xyz/inv"
    

    (No trailing slash.)

  • Clear caches:

    php artisan optimize:clear
    

B. Livewire v3 (recommended): set the script & update routes at runtime

Add this to routes/web.php (or AppServiceProvider::boot() if you prefer):

use Illuminate\Support\Facades\Route;
use Livewire\Livewire;

// Serve Livewire's JS from /inv/livewire/livewire.js
Livewire::setScriptRoute(function ($handle) {
    return Route::get('/inv/livewire/livewire.js', $handle)->name('livewire.js');
});

// Post updates to /inv/livewire/update
Livewire::setUpdateRoute(function ($handle) {
    return Route::post('/inv/livewire/update', $handle)->name('livewire.update');
});

This forces Livewire to generate:

<script src="/inv/livewire/livewire.js" data-update-uri="/inv/livewire/update"></script>

(Why this works: Livewire 3 supports configuring its endpoints at runtime; this is the official pattern for non-root installs.) (Stack Overflow, Laravel)

C. Livewire v2 alternative (if you’re still on v2)

  1. Publish config:
php artisan livewire:publish --config
  1. In config/livewire.php, set:
'asset_url' => '/inv',

This prepends /inv to /livewire/livewire.js. (For v2, asset_url is the documented way to support subdirectories.) (Livewire)

Tip: If you’ve previously published assets, you can also do
php artisan vendor:publish --force --tag=livewire:assets
then ensure they’re reachable under the subfolder. (mazer.dev)


2) Make NGINX (cPanel “NGINX Manager”) pass Livewire through

Because NGINX sits in front, it may try to serve /livewire/livewire.js as a static file or cache /livewire/update. Add a user include to bypass cache and route these URIs to Apache/PHP.

In your NGINX include for the domain (via cPanel NGINX Manager):

# Livewire endpoints under subfolder
location ^~ /inv/livewire/ {
    # do not cache ajax updates or dynamic js
    proxy_no_cache 1;
    add_header X-Accel-Buffering no;

    # If you proxy to Apache:
    proxy_pass http://apache_upstream;
    include proxy_params;

    # OR if you run PHP-FPM directly, use try_files to index.php:
    # try_files $uri /inv/index.php?$query_string;
}

(Exact upstream names vary per WHM setup. The key idea: don’t treat /inv/livewire/* as static; forward to PHP.) Guides on Livewire 404s with NGINX call out this pitfall explicitly. (inyomanjyotisa.medium.com, Laravel Geek, GitHub)


3) Update Apache/.htaccess for subfolder rewrites

In /home/mydomain/public_html/inv/.htaccess (or /public if that’s your document root), ensure you have a RewriteBase that matches the subfolder:

RewriteEngine On
RewriteBase /inv/

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

This helps Laravel generate correct relative URLs when behind Apache+cPanel in a subdirectory (it’s a common gotcha with subfolder installs).


4) Verify

  • Visit https://mydomain.xyz/inv/livewire/livewire.js in the browser — you should get a 200 OK (JS content).
  • Open the invoice link as a client; in DevTools → Network you should see POST /inv/livewire/update returning 200, not 404.
  • If still wrong, re-clear caches (optimize:clear) and check that your Blade layout includes @livewireScripts on portal pages.

5) Edge cases (pick what applies)

  • cPanel “public” exposure: make sure the public entry point is /inv/public (via a symlink or by moving the framework files one level up and keeping only public assets in webroot). Avoid serving the framework root directly.

  • SESSION path (rare, but in some cPanel proxies you may need):

    SESSION_DRIVER=cookie
    SESSION_SECURE_COOKIE=true
    SESSION_PATH=/inv
    
  • Caching/CDN: if any CDN is in front, exclude /inv/livewire/* from caching.


6) Why a subdomain often “just works”

Putting the app at a subdomain (e.g., invoices.mydomain.xyz) avoids path prefix issues entirely and simplifies NGINX/Apache rules. If you’re able to switch later, it’s the lowest-friction long-term setup for Livewire-based portals. (Several Invoice Ninja forum threads end up recommending this.) (forum.invoiceninja.com)


Quick copy-paste checklist

  1. .envAPP_URL=https://mydomain.xyz/inv
  2. routes/web.php → add the Livewire::setScriptRoute and setUpdateRoute block above.
  3. NGINX Manager include → add the location ^~ /inv/livewire/ block to bypass static caching and forward to PHP/Apache.
  4. .htaccess → ensure RewriteBase /inv/.
  5. php artisan optimize:clear
  6. Reload services; re-test invoice in client portal.

Hi,

I have encountered this issue a few times on self hosted via cpanel/whm. I tried many of the path related fixes/workarounds, only to hit further errors elsewhere. The easiest and only fix I found was to create a subdomain and reinstall using the method I outlined earlier. Hope that helps.

Thanks @SteveMc

As a test I created a subdomain that points to the current installation and then updated the APP_URL in the .env file to be APP_URL=“https://inv.mydomain.xyz”. I went into Settings >> client portal and the Login URL has been updated to https://inv.mydomain.xyz/client/login (no /inv in the URL anymore). I tested with an existing invoice and it gave me the same result (404 error). However, I created a new invoice and it looks like it is working. No 404 error and I was able to pay the invoice successfully. I confirmed that it’s marked paid in Invoice Ninja and my payment processor has the payment on file.

So now my question is, is it really necessary to go through the new backup/new installation/restoring data or can I proceed as is with no issues in the future? I assume that I should probably log out of the app and log back in using the new URL and I’m fine with that. I’m also okay with previous invoices and quotes not being available to view online. Or maybe there’s a way to update the URL for previously existing data? But I’m not sure if there are issues that may pop up that I’m not aware of.

One more quick update, after logging out of and back into the app using the new URL even old invoices are now able to be viewed online. Unless you can think of a reason why I shouldn’t proceed as-is then I don’t see a reason to go through the backup/new install/restore process.