malefici/metabase-bundle
A Laravel/PHP bundle for integrating Metabase analytics into your application. It provides configuration and helpers to connect your app with Metabase, making it easier to embed dashboards and questions and surface analytics within Laravel projects.
First, a fit check. This is a Symfony bundle (malefici/metabase-bundle), not a Laravel package. There is no service provider, no config/metabase.php, and no facade. Before investing time, decide which of two paths you're on:
firebase/php-jwt. Given 0 stars, 0 dependents, a single release (2024-03-17), and no ecosystem, this is the pragmatic choice for most Laravel apps.If you go with the package itself, minimal steps:
composer require malefici/metabase-bundle
config/bundles.php (Symfony's registry, not Laravel's bootstrap/providers.php).DependencyInjection/Configuration.php in vendor/malefici/metabase-bundle/ first. That file is the source of truth for every option.MetabaseEmbedService / a signer), and any Twig extension. If there's no Twig function, you're calling the service directly from your controller.First use case: render a single dashboard in an iframe, authenticated by a server-signed token. Controller builds the signed URL, view renders <iframe src="{{ $signedUrl }}">. That's the whole value proposition — everything else is parameterization.
The core flow. Metabase signed embedding works like this: you take the dashboard/question ID, optional params, and an expiry, put them in a JWT payload, sign with your embedding secret, and append it as the token query param on /embed/dashboard/<token>. The bundle wraps the signing step. Your job is the surrounding plumbing.
Thin controller, thick service. Don't sign tokens in the controller. Bind a small wrapper as a singleton in a Laravel service provider (or use the bundle's service in Symfony) and inject it:
final class MetabaseEmbedder
{
public function __construct(
private string $siteUrl,
private string $secret,
) {}
public function dashboardUrl(int $id, array $params = [], int $ttl = 600): string
{
$payload = [
'resource' => ['dashboard' => $id],
'params' => $params, // e.g. ['tenant_id' => [1]]
'exp' => time() + $ttl,
];
$token = JWT::encode($payload, $this->secret, 'HS256');
return rtrim($this->siteUrl, '/') . '/embed/dashboard/' . $token . '#bordered=true&titled=true';
}
}
Treat the bundle's signer as exactly this shape; if it doesn't expose a clean url() method, write yours and use the bundle only for config plumbing.
Parameterization is the real feature. A dashboard embedded for tenant A must never show tenant B's rows. Pass locked params when signing — the token is the authorization boundary, not the iframe. If a param must be user-adjustable (date range, category), exclude it from params and let Metabase's UI handle it; everything security-relevant gets baked into the token.
Expiry and caching. Signed tokens are cheap but not free, and they're short-lived (Metabase defaults to ~10 minutes). For a dashboard rendered on every page load, cache the signed URL keyed by a hash of (resource, params) with a TTL comfortably below exp — a few minutes. This avoids re-signing on every request and keeps clock skew from biting you.
Route + auth middleware. Expose GET /dashboards/{id} in your app, apply your normal auth middleware, resolve the user's tenant, then sign. The browser never talks to Metabase's API directly — the iframe loads a URL Metabase validates, and Metabase enforces the params you signed. Your middleware remains the only gate on who gets a token.
Blade component for reuse. Wrap the iframe in <x-metabase-dashboard :id="..." :params="..." /> so the iframe attributes (width, height, frameborder, allowtransparency, CSP-friendly loading="lazy") live in one place instead of scattered across views.
composer require this and get a working provider. If you're on pure Laravel, budget the 30–60 minutes to implement the signer yourself and reuse this bundle's config/DI structure as a reference. You'll likely end up with less code and full control.composer.json, and prefer wrapping it behind your own interface (MetabaseEmbedder) so you can swap to a hand-rolled signer without touching call sites.['tenant_id' => [1,2]]) and special characters need care in the JWT payload. Metabase expects the JSON array form; a string "1,2" silently matches nothing.X-Frame-Options and Content-Security-Policy: frame-ancestors on the Metabase side, and add your origin. A blank iframe in devtools is almost always this, not the token.resource, params, exp → load the raw embed URL in a browser tab (not in your app) → read Metabase server logs for the rejection reason → then look at your app's CSP. Skip steps and you'll chase phantom auth bugs.decorates:) or wrap it rather than editing vendor code. If it ships a Twig function, don't reuse it — define your own so the view layer stays portable to Blade.Configuration.php and the README before writing any config. The package's docs are sparse; the DI configuration node is the only reliable schema. Anything you guess about option names will be wrong.How can I help you explore Laravel packages today?