Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Metabase Laravel Package

laravolt/metabase

Laravel integration for Metabase, making it easy to embed analytics dashboards and questions in your app. Configure connections, generate signed embed URLs, and secure access with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin signer: it turns a Metabase dashboard/question ID plus a set of filters into a short-lived signed embed URL you can drop into an <iframe>. Almost all of your day-to-day work happens in two places — config/metabase.php and the facade/helper that returns the URL.

  1. Metabase side (do this first, it's the usual source of "blank iframe" support tickets):

    • Admin → Settings → Embedding → enable Static embedding.
    • Copy the embedding secret key (a UUID). This is not the API key, and not MB_ENCRYPTION_SECRET_KEY.
    • Open the dashboard → Sharing → enable embedding for that specific dashboard. An unshared dashboard silently fails to render.
  2. Laravel side:

composer require laravolt/metabase
php artisan vendor:publish --tag=metabase-config   # confirm tag name in README
METABASE_SITE_URL=https://metabase.example.com
METABASE_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  1. First use case — a single dashboard page:
// routes/web.php
Route::get('/reports/revenue', fn () => view('reports.revenue'));

// resources/views/reports/revenue.blade.php
<iframe
    src="{{ Metabase::url($dashboardId, ['tenant_id' => auth()->user()->tenant_id]) }}"
    frameborder="0" width="100%" height="900" allowfullscreen
></iframe>

Look first at: config/metabase.php (site URL, secret, token TTL if exposed), then the facade's public methods — the surface area is genuinely small, so read the class rather than the docs. Finally check the Blade component/directive if the package ships one, since that's what most teams end up using in views.

Upgrade note for 0.4.2: nothing in your application code needs to change unless you were reaching into the token-building internals (see Gotchas). This is a drop-in swap of lcobucci/jwt → firebase/php-jwt.

Implementation Patterns

Lock your filters in the token. The whole security model of static embedding is that filter values come from the signed JWT, not the URL. Pass tenant/role/customer IDs as locked parameters so a user can't edit the iframe query string and see another tenant's numbers:

Metabase::url($dashboardId, [
    'tenant_id'   => 'locked',       // rendered but not user-editable
    'date_range'  => 'past30days',  // editable in the Metabase UI
]);

Only expose as editable the parameters you're happy for a user to change.

Generate URLs at render time, never cache them. Embed tokens are short-lived (Metabase caps them, and the package will set an exp). Put URL generation in the controller/Blade render, not in a cached view fragment, a queued job payload, or an API response you store. If you need long-lived pages, expose a tiny refresh endpoint that returns a fresh URL and have the front end swap the iframe.src.

Multi-tenant / multi-workspace setups. Keep a single service class that resolves the secret per tenant and sets config before delegating to the package, rather than calling the facade from controllers directly:

final class TenantMetabase
{
    public function url(Tenant $tenant, int $dashboardId, array $params = []): string
    {
        config(['metabase.secret_key' => $tenant->metabase_secret]);

        return Metabase::url($dashboardId, $params + ['tenant_id' => $tenant->id]);
    }
}

Bind the package's contract/instance in a service provider if it's concrete, so tests can swap it.

Testing. Because 0.4.2 uses firebase/php-jwt, you can now decode and assert on the generated token directly in feature tests without pulling in a builder API:

$url = Metabase::url(1, ['tenant_id' => 'acme']);
parse_str(parse_url($url, PHP_URL_FRAGMENT) ?? '', $frag); // or query, per Metabase's format
$claims = JWT::decode($frag['token'], new Key(config('metabase.secret_key'), 'HS256'));

$this->assertSame(1, $claims->resource->dashboard);
$this->assertSame('acme', $claims->params->tenant_id);

That's the cheapest regression test for "someone broke row-level security by dropping the locked param."

Composition. Dashboards rarely live alone — pair the iframe with a nav that keys off dashboard IDs in config, and keep dashboard IDs in config/ or an enum rather than magic numbers sprinkled across Blade files.

Gotchas and Tips

  • Wrong secret is the #1 failure. Embedding secret ≠ API key ≠ encryption secret. A bad secret produces a valid-looking iframe URL that Metabase rejects — usually a blank frame or a generic error, with nothing in your Laravel logs.
  • Silent failures are the norm. Since the failure happens in the browser, debug in the network tab: look at the POST /api/embed/dashboard/:token (or the embed HTML response) and its status. Decode the token locally to check exp, resource, and params.
  • exp in the past = blank frame. Clock skew between your app server and Metabase can bite you. Keep TTLs modest (minutes) and don't round down aggressively.
  • Dashboard must be embedding-enabled and published. Changing a dashboard's sharing settings in Metabase can break a working page with zero code changes — worth a note in your runbook.
  • Reverse-proxy/CSP. If Metabase sits behind nginx or your app sets a strict CSP, frame-src/frame-ancestors on your page can block the embed. Metabase's MB_EMBEDDING_APP_ORIGIN must match your Laravel origin.
  • 0.4.2 / firebase/php-jwt specifics. This is a patch bump but a real dependency swap:
    • If any of your tests or helpers imported Lcobucci\JWT\* because it was transitively installed, they'll break — firebase/php-jwt has no builder; it's JWT::encode($payload, $key, 'HS256') and JWT::decode($token, new Key($key, 'HS256')). The algorithm argument is now required on encode.
    • firebase/php-jwt throws SignatureInvalidException / ExpiredException on decode rather than returning null — catch explicitly if you decode anywhere.
    • Check you don't pin lcobucci/jwt at a version another package needs; Composer will now resolve it independently, which can shift a constraint. Run composer why lcobucci/jwt after upgrading to confirm nothing else depended on the old pin.
    • firebase/php-jwt has a PHP floor (8.x for recent majors) — verify against your CI matrix if you support older PHP.
  • Don't config:cache a tenant-specific secret. If you mutate config() per request (multi-tenant pattern above), that's fine at runtime, but never bake a per-tenant secret into a cached config file.
  • Extension points: the published config file, the facade root binding (swap for a fake or a decorated implementation), and — if the package exposes it — a custom token generator. That last one is the only thing 0.4.2 could plausibly break for you; grep your codebase for Lcobucci and for any class extending the package's signer before upgrading.
  • Pin the minor. Given the token layer has changed under a patch version, constrain to ^0.4.2 rather than ^0.4 if you want to review future signing changes deliberately.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core