The Problem
Metabase is a 10-project portfolio masquerading as a single BI platform. The frontend alone contains 9615 files across 8351 code files, with TypeScript and TSX comprising 61% of the codebase. For a team looking to embed analytics or self-host BI, the scale is intimidating: 2242 internal call edges, 2137 named functions, and 43 classes create a graph where popover is called from 116 places and t from 50. The enterprise edition adds another 2131 files with embedding and permission logic. No single architecture connects it all—the repo is a deliberate collection of semi-independent projects (frontend, enterprise, e2e, resources, bin, etc.) sharing only a npm build pipeline and Docker deployment.
What This Does
The repository delivers three overlapping capabilities: embedded analytics via an SDK with Angular/Next.js host apps, self-hosted BI with dashboard and document workflows, and AI-assisted querying through Metabot. Key files driving these:
frontend/src/metabase/app.js:120—initentry point reaching 12 functions, the primary bootstrapping nodebin/embedding-sdk/fixup-types-after-compilation.js:220—runentry point reaching 6 functions, type-post-processing for the SDKenterprise/frontend/src/metabase-enterprise/audit_app/index.js:27—menuItemFunctionreaching 30 functions, called by nothing else in the repo (dead entry)e2e/snapshot-creators/default.cy.snap.js:77—setupreaching 66 functions, e2e test orchestrationenterprise/frontend/src/metabase-enterprise/advanced_permissions/index.js:50—initializePluginreaching 30 functions, permission system initialization
The e2e support layer is a hub: e2e/support/helpers/e2e-ui-elements-helpers.js defines popover, hovercard, main, menu, modal with 101 functions called from 88 other files. resources/frontend_client/app/iframeResizer.js defines the DOM resizing primitives (h, O, R, o, i) called from 36 files. The data selection flow runs through frontend/src/metabase/querying/common/components/DataSelector/DataSelector.jsx (32 functions) and its enterprise counterpart at enterprise/frontend/src/embedding/data-picker/DataSelector/DataSelector.jsx (26 functions, 1 class).
How It Is Wired
Execution starts at three documented entry points: init in frontend/src/metabase/app.js:120 (reaches 12 functions, called from 1 place), run in bin/embedding-sdk/fixup-types-after-compilation.js:220 (reaches 6 functions, called from 1 place), and main in e2e/runner/start-backend.js:5 (reaches 16 functions, called from 2 places). The internal call graph shows t (translation/i18n) as a ubiquitous hub—render -> t x17, title -> t x13, verboseName -> t x64—meaning any i18n change ripples across the entire UI. mapStateToProps is another high-degree node, feeding getIsEditing x20, getTable x13, getUser x12, getDatabase x9, and eleven other selector functions; a single prop change can re-render numerous components. UNSAFE_componentWillMount -> fetchContainerData x13 and UNSAFE_componentWillReceiveProps -> clearState x13 reveal legacy lifecycle methods still driving data fetching, a pattern that couples component mounting to container state in ways a modern React migration would reconsider. The embedding SDK’s type-fixup script (fixup-types-after-compilation.js) runs as a post-compilation step, bridging the gap between the TypeScript source and the runtime SDK payload.
How To Use It
Setup: The repository uses npm as its package manager. Install with npm install across the monorepo structure, or target a single project—frontend/ for the web UI, enterprise/ for extended permissions, bin/embedding-sdk/ for the SDK. A Dockerfile at the root and bin/docker/Dockerfile provide containerized builds; the .devcontainer/Dockerfile uses mcr.microsoft.com/vscode/devcontainers/java:21 for development environments. No root-level Makefile exists; build scripts are npm lifecycle scripts.
Configuration: Environment variables are defined in .env.example at the root. The Docker setup references docker-compose.yml in cross-version/. For the embedding SDK, bin/embedding-sdk/templates/package.json lists SDK-specific dependencies. No secret management beyond the .env pattern is configured—production deployments must supply their own database connection strings, JWT secrets, and SMTP credentials via environment variables.
Running it: To start the development server, npm run dev from the root (as implied by the scripts in package.json, though the exact command is not in the extracted evidence). For production, the Dockerfile builds and runs metabase/metabase image. The embedding SDK can be tested via the e2e host apps in e2e/embedding-sdk-host-apps/ (angular-20, next-15-pages-router, next-15-app-router). No explicit start command is documented beyond the Docker workflow.
Real-World Use
A product team wants to embed self-serve analytics into their SaaS app. They pull the bin/embedding-sdk, initialize it with initializePlugin from enterprise/frontend/src/metabase-enterprise/advanced_permissions/index.js:50, and render chart components via the SDK’s embedded endpoints. The data selector (DataSelector.jsx) lets end-users choose databases and fields without writing SQL. Dashboards can be embedded whole or chart-by-chart, with permissions governed by the enterprise edition’s advanced permissions module. The team runs npm run build and serves the static output, or wraps the build in their own CI pipeline. Metabot, the AI query assistant, is opt-in and configured through the admin UI, not exposed through the SDK by default.
Code Health & Issues
The static analysis found 8 findings across the SDLC:
- [HIGH] Remove the committed database dump -
resources/sample-database.db.mv.db. A dump leaks other people's data, turning a hygiene problem into a notification obligation. Remove and purge from history; if it held personal data, assess as a disclosure. - [MEDIUM] Declare least-privilege permissions for GITHUB_TOKEN -
.github/workflows/build-for-release.yml. With no declaration the token inherits the repository default, so any injected step can push commits or mint releases from inside your own CI. Addpermissions: contents: readat the top of the workflow and widen per job only where needed. - [MEDIUM] Enable Dependabot or Renovate. 8 manifest(s), no update bot configured. Without a bot a published advisory sits unpatched until someone audits by hand. Commit
.github/dependabot.ymlcovering the repo ecosystems plus github-actions. - [MEDIUM] Enable Dependabot or Renovate. Pin the container base image by digest -
.devcontainer/Dockerfile.mcr.microsoft.com/vscode/devcontainers/java:21is an untagged mutable base; today's build and last month's contain different libc and a different CVE set, with no record of which shipped. Useimage:tag@sha256:<digest>and enable Dependabot's docker ecosystem. - [MEDIUM] Gate pull requests on a dependency vulnerability scan -
.github/workflows. No dependency scan in CI. This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it. Add dependency-review-action on pull_request, or osv-scanner on push and a schedule. - [MEDIUM] Move large binaries to Git LFS or out of the repository -
docs/questions/images/map-tiles.png. 1 blob over 5MB:map-tiles.png9.0MB. One repository here carries twenty blobs over 5MB including a 10.9MB spreadsheet, so every clone and every CI checkout pays for data nobody diffs. Track those extensions with LFS, or move datasets to object storage and fetch them in a setup step. - [MEDIUM] Remove generated build output from version control -
bin. 70 generated files committed. The reviewer readssrc, production runsdist, and a stale bundle means the fix that was reviewed is not the code that executes. Gitignore the output directory and build in CI. - [MEDIUM] Review the install lifecycle script and disable scripts in CI -
package.json.preinstall,postinstall(fetches from the network). Install scripts are the execution vector for every npm compromise from event-stream onward, and a postinstall that fetches a binary makes the build depend on a URL nobody reviews. Move the work into an explicit build step, or setignore-scriptsin CI and run it by name.
The Bottom Line
Metabase is a capable, widely-used BI platform that self-hosts well and embeds via a functional SDK, but it comes at the cost of a large, loosely-coupled codebase where no single architecture governs the whole system. The 10-project structure means you can pull only the pieces you need—the frontend, the embedding SDK, or the enterprise permission layer—but you’ll still inherit the same npm build pipeline, Docker deployment model, and the hygiene findings listed above. Teams comfortable with a monorepo of ~18K files and willing to address the medium-severity CI and dependency management gaps will find it practical; those seeking a lightweight, single-purpose library should look elsewhere.