{"id":29097381,"url":"https://github.com/lambdaisland/embedkit","last_synced_at":"2025-08-31T05:37:05.245Z","repository":{"id":46226847,"uuid":"338260494","full_name":"lambdaisland/embedkit","owner":"lambdaisland","description":"Metabase as a Dashboard Engine","archived":false,"fork":false,"pushed_at":"2023-12-05T06:50:00.000Z","size":80,"stargazers_count":34,"open_issues_count":1,"forks_count":0,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-06-08T21:35:38.086Z","etag":null,"topics":["bi","metabase"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lambdaisland.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-02-12T08:21:46.000Z","updated_at":"2025-04-29T12:01:30.000Z","dependencies_parsed_at":"2024-04-17T14:20:42.208Z","dependency_job_id":"274e32fa-d51a-4f00-96b3-d4904c77955e","html_url":"https://github.com/lambdaisland/embedkit","commit_stats":{"total_commits":61,"total_committers":3,"mean_commits":"20.333333333333332","dds":0.3278688524590164,"last_synced_commit":"94e8c08fc3dead370e12e83f238a3f2e8bf3a40c"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/lambdaisland/embedkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Fembedkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Fembedkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Fembedkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Fembedkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdaisland","download_url":"https://codeload.github.com/lambdaisland/embedkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Fembedkit/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260159093,"owners_count":22967332,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bi","metabase"],"created_at":"2025-06-28T13:42:04.061Z","updated_at":"2025-06-28T13:42:05.227Z","avatar_url":"https://github.com/lambdaisland.png","language":"Clojure","funding_links":["http://opencollective.com/lambda-island"],"categories":[],"sub_categories":[],"readme":"# lambdaisland/embedkit\n\n\u003c!-- badges --\u003e\n\u003c!-- [![CircleCI](https://circleci.com/gh/lambdaisland/embedkit.svg?style=svg)](https://circleci.com/gh/lambdaisland/embedkit) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/embedkit)](https://cljdoc.org/d/lambdaisland/embedkit) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/embedkit.svg)](https://clojars.org/lambdaisland/embedkit) --\u003e\n\u003c!-- /badges --\u003e\n\nUse Metabase as a dashboard engine\n\n\u003c!-- opencollective --\u003e\n\n\u0026nbsp;\n\n\u003cimg align=\"left\" src=\"https://github.com/lambdaisland/open-source/raw/master/artwork/lighthouse_readme.png\"\u003e\n\n\u0026nbsp;\n\n## Support Lambda Island Open Source\n\nembedkit is part of a growing collection of quality Clojure libraries and\ntools released on the Lambda Island label. If you are using this project\ncommercially then you are expected to pay it forward by\n[becoming a backer on Open Collective](http://opencollective.com/lambda-island#section-contribute),\nso that we may continue to enjoy a thriving Clojure ecosystem.\n\n\u0026nbsp;\n\n\u0026nbsp;\n\n\u003c!-- /opencollective --\u003e\n\n## Sponsors\n\nInitial development of EmbedKit is generously sponsored by [Eleven](https://runeleven.com).\n\n## Features\n\n\u003c!-- installation --\u003e\n## Installation\ndeps.edn\n\n```\nlambdaisland/embedkit {:mvn/version \"0.0.56\"}\n```\n\nproject.clj\n\n```\n[lambdaisland/embedkit \"0.0.56\"]\n```\n\u003c!-- /installation --\u003e\n\n## Rationale\n\nMetabase is able to talk to many different data sources, and to turn what it\nfinds into attractive dashboards, with many options for how to visualize the\nresults. These dashboards can then be embedded as iframes.\n\nThis library allows you to use Metabase as a dashboard engine for your\napplication, creating embeddable dashboards on the fly based on a pure data\n(EDN) specification of the dashboard, and the cards thereon.\n\nIt takes care of all the low level plumbing, as well as many inconsistencies in\nMetabase's API, and provides higher-level operations for creating multiple\nrelated entities in one go.\n\nIt uses content-addressed caching to reuse previously created cards and\ndashboards. The assumption when using this library is that these entities are\nimmutable, if you need a different one, just create a different one.\n\n## Usage\n\nLet's start with a teaser \n\n``` clojure\n(def conn (e/connect {:user \"admin@example.com\" :password \"...\" :secret-key \"...\"}))\n\n(def db (e/find-database conn \"orders\"))\n\n(def dashboard (-\u003e\u003e (e/dashboard {:name \"My sales dashboard\"\n                                  :cards [{:card (-\u003e (e/native-card {:name \"Monthly revenue\"\n                                                                     :database db\n                                                                     :sql {:select [\"month\" \"SUM(amount) AS total\"]\n                                                                           :from [\"orders\"]\n                                                                           :group-by [\"month\"]\n                                                                           :order-by [\"month\"]}})\n                                                     (e/bar-chart {:x-axis [\"month\"]\n                                                                   :y-axis [\"total\"]}))\n                                           :width 12 :height 10}]})\n                    (e/find-or-create! conn)))\n\n;; Open the dashboard in the browser, REPL helper for local testing\n(r/browse! dashboard)\n\n;; Get an embed-url that you can use in an iframe\n(e/embed-url conn dashboard)\n```\n\nLet's pick that apart, first you need to create a connection:\n\n``` clojure\n(def conn (e/connect {:user \"admin@example.com\"\n                      :password \"...\"\n                      ;; See the metabase embed settings for this\n                      :secret-key \"...\"\n                      :host \"localhost\"\n                      :port 3000\n                      :https? false?}))\n```\n\nThis does the initial HTTP call to Metabase to request an authorization token.\nThe result is a record that encapsulates everything we need to know to talk to\nthe API. There are a few more options related to the underlying HTTP client.\nThis also wraps an atom which serves as a cache.\n\nAfter connecting you are expected to also call `populate-cache`. This will allow\nEmbedKit to reuse cards and dashboards based on their content hash.\n\n``` clojure\n(e/populate-cache conn)\n```\n\nNext you can find the database you want to create dashboards for.\n\n``` clojure\n(def db (e/find-database \"orders\"))\n```\n\nThis is just a little helper to find a database in Metabase by name. We\ngenerally go to great lengths to prevent having to deal with Metabase's\nincremental ids outside of Metabase. This needs to fetch the full list of\ndatabases, but these are then cached in memory.\n\nEmbedKit is heavily data-driven. You first create an EDN representation of the\nentity you want to create. For \"questions\" (what Metabase internally calls\nCards) you start with the `native-card` function. Currently only native (SQL)\nqueries are supported.\n\n```clojure\n(e/native-card {:name \"Monthly revenue\"\n                :database db\n                :sql {:select [\"month\" \"SUM(amount) AS total\"]\n                      :from [\"orders\"]\n                      :group-by [\"month\"]\n                      :order-by [\"month\"]}})\n```\n\nYou can pass a `:database` entity or a `:database-id` numeric id, if you have\nit. `:sql` can be a string or a map, if it's a map we run it through HoneySQL.\n\nThis returns an \"entity map\", which looks like this:\n\n``` clojure\n{:lambdaisland.embedkit/type :card\n :name \"Monthly revenue\"\n :database_id {:id 2}\n :query_type \"native\"\n :dataset_query {:database {:id 2}\n                 :type \"native\"\n                 :native\n                 {:query \"SELECT month SUM(amount) AS total FROM orders GROUP BY month ORDER BY month\"}}\n :display \"table\"\n :visualization_settings {}\n :lambdaisland.embedkit/variables {}}\n```\n\nThe keys that are namespaced (`:lambdaisland.embedkit/type` and\n`:lambdaisland.embedkit/variables`) are for EmbedKit's own use, to figure out\nthe correct API endpoint for a given resource, and to correctly wire up multiple\nentities (think: dashboard -\u003e dashboard-card -\u003e card), everything else is in the\nformat that the Metabase API expects.\n\nOnce you have an entity map like this you can run it through `find-or-create!`.\n\n``` clojure\n(e/find-or-create! conn (my-card))\n```\n\nThis will check the local cache, using a hash of the data. If it didn't find a\nmatch, then a new entity gets created. Either way what you get back is the\nrepresentation of this entity as returned by the Metabase API, augmented with\nour embedkit-specific keys.\n\nThere are also functions which adjust the entity description, for instance\n`bar-chart`, which changes how the result is rendered.\n\n``` clojure\n(-\u003e (e/native-card {...})\n    (e/bar-chart {:x-axis [\"...\"] :y-axis [\"...\"]})\n```\n\nUsing these you can build up your own functions, describing the cards you are\nwant to display. Finally you get to put them together in a dashboard.\n\n``` clojure\n(e/find-or-create!\n conn\n (e/dashboard {:name \"My sales dashboard\"\n               :cards [{:card (my-card-fn)\n                        :x 5 :y 0\n                        :width 12 :height 10}]}))\n```\n\nThis will create the dashboards, the cards, and then the dashboard cards.\n\nFinally you can pass the result of `find-or-create!` to `embed-url` to get a URL\nyou can use to create an iframe. The result is a `lambdaisland.uri`, call `str`\non it to get the URL as a string.\n\n### Variables\n\nMetabase allows you to create \"variables\" for queries/cards, hook these up to\n\"parameters\" of dashboards, and fill them in when creating embed-urls. This\nrequires definitions in three different places. This is one of the things that\nis extremely opaque to do via the API. We simplify this by taking variable\ndefinitions on the cards, and wiring these up automatically to dashboard-cards,\nand exposing them in embed urls via the signed payload (\"locked\" parameters).\n\nThe main use case so far is to allow reusing a single dashboard definition\ncontaining some placeholder variables.\n\n``` clojure\n(e/native-card {:variables {:category {}} \n                :sql {:where [:= \"category\" \"{{category}}\"})\n```\n\n- Use `{{var_name}}` placeholders in your SQL\n- Add a corresponding entry in the `:variables` map. The associated key is a map\n  with variable-specific options, like `:type`. It can be left empty. The\n  default `:type` is `\"text\"`.\n  \nWhen using this to create a dashboard, corresponding parameter will be created\nfor the dashboard, which will be set to \"embeddable, locked\". That is, you can\nset them via the JWT-signed payload, but the user can't set them via the URL.\n\nWhen calling `embed-url` you can pass values for these variables.\n\n``` clojure\n(e/embed-url conn (e/find-or-create! (my-dashboard)) {:variables {:category \"toys\"}})\n```\n\n### Other utilities\n\n#### Initialize the metabase\nSee the example file from `repl_sessions/init.clj`\n\n```\n(def config {:user \"admin@example.com\"\n             :password \"xxxxxx\"})\n;; create admin user and enable embedded\n(setup/init-metabase! config)\n\n;; setup embedding secret key\n(e/mb-put conn*\n          [:setting :embedding-secret-key]\n          {:form-params {:value \"6fa6b6600d27ff276d3d0e961b661fb3b082f8b60781e07d11b8325a6e1025c5\"}})\n\n;; get the embedding secret key\n(def config* (assoc config\n                    :secret-key (get\n                                 (setup/get-embedding-secret-key conn*)\n                                 :value)))\n\n;; begin normal connection\n(def conn (e/connect config*))\n```\n\n#### Create a new db connection\nSee the example file from `repl_sessions/create_db_conn.clj`\n\n```\n;; Example for Postgres \n(def db-conn-name \"metabase-db-connection-name\")\n(def engine \"postgres\")\n(def details {...}) \n(setup/create-db! conn db-conn-name engine details)\n```\n\n#### Trigger the sync of a db schema and field values\n\n```\n(e/trigger-db-fn! conn \"example_tenant\" :sync_schema)\n(e/trigger-db-fn! conn \"example_tenant\" :rescan_values)\n```\n\n#### ID lookup utilities\nFor human, it is natural to remember the name of an entity, be it a database, \ndatabase schema, or a table. On the other hand, inside metabase, these entities are \nall represented by numeric IDs.\nThat is why we also provide a series of ID lookup utilities:\n```\n(find-database ...)  ;; get the database entity information which include db-id through database-name\n(table-id ...)       ;; find out field-id by database-name, schema-name, table-name\n(field-id ...)       ;; find out field-id by database-name, schema-name, table-name, field-name\n(user-id ...)        ;; find out user-id by email\n(group-id ...)       ;; find out group-id by group-name\n```\n\n### Metabase version \u0026 related issues\n\n#### Supported Metabase version\n0.44.0 or later\n\n#### Pagination\nThe newest release version of embedkit is developed along with metabase version `0.44.6`.\nAccording to [here](https://github.com/metabase/metabase/wiki/What%27s-new-in-0.40.0-for-Metabase-REST-API-clients), metabase should have `/api/user` and `/api/database` supporting pagination feature. However, real world testing shows that only `/api/user` has the pagination feature. Also, the `total` in the return result of `/api/user` is actually refering to the total number of users rather than the total number of users with respect to the query.\n\n\u003c!-- contributing --\u003e\n## Contributing\n\nEveryone has a right to submit patches to embedkit, and thus become a contributor.\n\nContributors MUST\n\n- adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide)\n- write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*`\n- agree to license their contributions as MPL 2.0.\n- not break the contract with downstream consumers. `**`\n- not break the tests.\n\nContributors SHOULD\n\n- update the CHANGELOG and README.\n- add tests for new functionality.\n\nIf you submit a pull request that adheres to these rules, then it will almost\ncertainly be merged immediately. However some things may require more\nconsideration. If you add new dependencies, or significantly increase the API\nsurface, then we need to decide if these changes are in line with the project's\ngoals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template),\nand collecting feedback on it.\n\n`*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution.\n\n`**` As long as this project has not seen a public release (i.e. is not on Clojars)\nwe may still consider making breaking changes, if there is consensus that the\nchanges are justified.\n\u003c!-- /contributing --\u003e\n\n\u003c!-- license --\u003e\n## License\n\nCopyright \u0026copy; 2021 Arne Brasseur and Contributors\n\nLicensed under the term of the Mozilla Public License 2.0, see LICENSE.\n\u003c!-- /license --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Fembedkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdaisland%2Fembedkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Fembedkit/lists"}