Two ways to deploy Medusa across an organization: roll the extension to your whole fleet on Medusa Cloud, or run the entire stack on your own infrastructure (on-prem, your VPC, or air-gapped). Every step is below.
Medusa has the same product on both paths — an in-browser DLP extension managed from a dashboard. The only difference is where the control plane lives.
Org-wide rollout (Cloud)
We host the dashboard + backend at medusasec.com. You push the extension to your fleet via MDM and manage policy in the dashboard. Fastest to roll out.
Best for: most teams. Setup: ~15 minutes.
Self-hosting (on-prem / your cloud)
One command stands up everything on your own box — database, auth, and dashboard, all in Docker (no external accounts). Findings, policies, and telemetry never leave your network — works fully air-gapped. Medusa only issues an offline license key.
Best for: regulated / air-gapped orgs. Setup: ~1 afternoon (or one command).
One thing is identical on both paths
All DLP detection runs inside each browser via a bundled on-device model. Scanned text never leaves the endpoint on either path — the control plane only ever receives metadata (category, action, severity), never the content.
Medusa endpoints are managed devices: IT issues the extension to every managed browser and configures it centrally — the end user does nothing and can't change what's monitored. There are exactly two moving parts:
The extension reads the org key from chrome.storage.managed and auto-enrolls on install, on startup, and whenever the managed key changes. The user never sees or types it.
In the dashboard, go to Settings → API keys and generate a new org key. One key can enroll your entire fleet; per-endpoint policy is still set per browser once they appear.
Org key (shown once at creation): sk-med_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Copy it now
The full key is shown only once— it's stored hashed (SHA-256). Save it in your MDM secret store before leaving the page.
In the Google Admin console: Devices → Chrome → Apps & extensions → Users & browsers. Add the extension by its Web Store ID, set Force install, then paste this into Policy for extensions:
{
"org_key": { "Value": "sk-med_YOUR_ORG_KEY" }
}That's it — managed browsers install Medusa silently and enroll on next launch.
Self-host override (optional)
Pointing managed browsers at your own backend? Add "control_plane_url": { "Value": "https://your-host" } to the same JSON. (For self-host you still need a per-org build — see the self-hosting section.)
Force-install via ExtensionInstallForcelist, then push the managed policy via the Chrome 3rdpartypolicy. The registry equivalent (deploy the same via Intune's Settings Catalog or an ADMX GPO):
# Force-install HKLM\Software\Policies\Google\Chrome\ExtensionInstallForcelist 1 = "<EXTENSION_ID>;https://clients2.google.com/service/update2/crx" # Push the org key HKLM\Software\Policies\Google\Chrome\3rdparty\extensions\<EXTENSION_ID>\policy org_key = "sk-med_YOUR_ORG_KEY"
Intune (no registry)
In Intune use Devices → Configuration → Settings catalog → Google Chrome: set Configure the list of force-installed apps and extensions, then Configure extension management settings with the same JSON payload from the Google Admin step. No registry editing required.
Deliver a configuration profile for the com.google.Chrome (or com.microsoft.Edge) preference domain:
<key>ExtensionInstallForcelist</key>
<array>
<string>EXTENSION_ID;https://clients2.google.com/service/update2/crx</string>
</array>
<key>3rdparty</key>
<dict>
<key>extensions</key>
<dict>
<key>EXTENSION_ID</key>
<dict>
<key>org_key</key><string>sk-med_YOUR_ORG_KEY</string>
</dict>
</dict>
</dict>In Jamf, deliver this as an Application & Custom Settings payload (Preference Domain com.google.Chrome) scoped to your managed Macs.
Edge uses the same policy structure as Chrome, but under the Microsoft Edge keys and with a different update URL and the Edge Add-ons extension ID:
# Windows registry (Edge) HKLM\Software\Policies\Microsoft\Edge\ExtensionInstallForcelist 1 = "<EDGE_EXTENSION_ID>;https://edge.microsoft.com/extensionwebstorebase/v1/crx" HKLM\Software\Policies\Microsoft\Edge\3rdparty\extensions\<EDGE_EXTENSION_ID>\policy org_key = "sk-med_YOUR_ORG_KEY"
On macOS, use the same plist as above but with the com.microsoft.Edge preference domain.
As managed browsers come online they appear on the Browsers page. From there:
AKIAIOSFODNN7EXAMPLE) into ChatGPT and watch the finding appear in Findings.Replace EXTENSION_ID
Use the published Chrome Web Store / Edge Add-ons ID once your listing is live. For pre-publish testing, load the extension unpacked and paste the org key in the extension's Options page.
Run the entire stack — dashboard and control plane — on your own infrastructure. Nothing talks to Medusa's cloud or to Supabase-the-company: your telemetry, policies, findings, and approvals stay inside your network. Medusa only issues an offline-verified license key.
Employee browsers Your infrastructure
┌────────────────┐ ┌──────────────────────────────┐
│ Medusa │ enroll │ Supabase (yours) │
│ extension │─policy──────▶│ • Postgres + RLS │
│ (per-org build)│ telemetry │ • Auth (GoTrue) │
│ • on-device │◀─realtime────│ • Edge Functions (5) │
│ DLP model │ push │ • Realtime • Storage │
└────────────────┘ │ │
│ Medusa dashboard container │
│ • Next.js UI + API routes │
│ • reads license offline │
└──────────────────────────────┘“Supabase” here = the open-source software, not a cloud you call
Medusa's control plane is built on Supabase's open-source stack (Postgres + GoTrue + PostgREST + Realtime + the Deno edge runtime), which you run yourself with Docker. The database is a plain PostgreSQL instance you own. There is no agent to install and no model server — detection runs entirely inside each browser.
Medusa is closed-source, so you never receive the dashboard source code. Instead the dashboard ships as a prebuilt container image — the compiled app, not the repo. Three facts make this work:
1. You pull an image, not source
The image lives at ghcr.io/joshmaster2165/medusa-dashboard . It's a private, license-gated image — Medusa grants Enterprise customers docker pull access on purchase. A container image is the compiled app, never your git repo (and the DLP model ships in the extension, not here).
2. One image, any Supabase (runtime config)
The image is built once with placeholder config; its entrypoint injects your Supabase URL + keys from .env.selfhost into the bundle at container start. So the same image works for every customer — no per-customer build.
3. Self-host is an Enterprise capability
Self-host (with SAML, MSSP multi-tenant, and air-gap) is part of the Enterprise tier. You receive a signed MEDUSA_LICENSE_KEY (verified offline with Ed25519 — works air-gapped) plus image pull access on purchase.
The two other pieces, also without source
Backend kit — a medusa-selfhost-kit.tar.gz (attached to each release) with the DB migrations + 5 edge functions + setup scripts, so you provision your own Supabase. It contains schema and control-plane glue only — not the dashboard app or the model.
Per-org extension— because a Chrome manifest's host permissions are static, each self-host org needs a build that allow-lists your origins. The build produces the unpacked zip plus a signed .crx + update.xml for MDM force-install (see Step 3).
One docker run stands up the entire stack — database, auth, edge functions, and dashboard — on your box. No repo, no Node, no Supabase account. The installer image carries the provisioning kit and drives your Docker daemon; the dashboard is pulled as the published, runtime-configured image.
# the dashboard image is private (license-gated) — log in with the token we issue echo $GHCR_TOKEN | docker login ghcr.io -u <your-github-user> --password-stdin docker run --rm -it \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /opt/medusa:/opt/medusa -e MEDUSA_HOME=/opt/medusa \ -e DASHBOARD_URL=https://dlp.your-company.com \ -e SUPABASE_PUBLIC_URL=https://supabase.your-company.com \ ghcr.io/medusasec/medusa-selfhost:latest
It stages Supabase's official stack, generates every secret, applies all migrations, deploys the 5 edge functions, and brings everything up. Re-running is safe (secrets preserved).
Two mount notes
MEDUSA_HOME must be the same pathon your host and in the container (the stack's bind mounts resolve on the host daemon), and the Docker socket mount lets the installer bring the stack up on your host. All stack state lives under $MEDUSA_HOME/stack.
Prefer to run it yourself from source (or air-gapped without the private image)? Use the one-command script or the bring-your-own paths below.
For the one-command all-in-one below (the recommended path — it stands up the database and everything else for you), that's the whole list:
medusa-selfhost-kit.tar.gz) — issued on purchase.You do NOT need a Supabase account
The all-in-one bundles the open-sourceSupabase stack and runs it in Docker on your own box — there is no external account to create and nothing to “point at”. A Supabase Cloud project + the Supabase CLI are only needed for the bring-your-own-Supabase alternative, further down.
This brings up everything on your own box — a PostgreSQL database you own, auth, the 5 control-plane edge functions, and the dashboard — in Docker. No external Supabase account, nothing to link.
tar -xzf medusa-selfhost-kit.tar.gz && cd medusa-selfhost-kit npm install DASHBOARD_URL=https://dlp.your-company.com \ SUPABASE_PUBLIC_URL=https://supabase.your-company.com \ node scripts/selfhost-up.mjs
That one command:
POLICY_SIGNING_SECRET) and applies all DB migrations (tables, RLS, org foundation, SIEM, approvals);extension-register, gateway-policy, gateway-events, dlp-approval-request, dlp-approval-status;“Supabase” here is not a cloud account
Supabase is the open-source Postgres + auth + API stack that runs in Docker on your server. The all-in-one bundles it, so your data (findings, policies, telemetry) never leaves your infrastructure and nothing calls out to Supabase's cloud. Works fully air-gapped.
Point both hostnames at this box behind TLS (dlp.… → the dashboard, supabase.… → the API gateway). The script also generates a ready-to-run Caddy config for automatic HTTPS — see SELF_HOSTING.md. Re-running the command after a URL change is safe (it preserves secrets and self-heals the dashboard).
Skip this if you used the one-command above. This path is only for teams who already run a Postgres / Supabase they want Medusa to use (a self-hosted Supabase, or a Supabase Cloud project). It needs the supabase CLI + Node 20+.
With the supabase CLI logged in (supabase login) and your project linked (supabase link --project-ref <ref>):
SUPABASE_PROJECT_REF=<your-project-ref> \ DASHBOARD_URL=https://dlp.your-company.com \ node scripts/selfhost-setup.mjs
Applies all migrations, deploys the 5 edge functions, and sets the secrets including POLICY_SIGNING_SECRET (auto-generated if omitted). Add --dry-run to preview first, and save the POLICY_SIGNING_SECRET — the dashboard must verify with the same value.
cp .env.selfhost.example .env.selfhost # fill NEXT_PUBLIC_SUPABASE_URL / _ANON_KEY / SUPABASE_SERVICE_ROLE_KEY from your # project, NEXT_PUBLIC_APP_URL, the SAME POLICY_SIGNING_SECRET, and MEDUSA_LICENSE_KEY echo $GHCR_TOKEN | docker login ghcr.io -u <your-github-user> --password-stdin docker compose -f docker-compose.selfhost.yml up -d
Air-gapped? Medusa ships the image as medusa-dashboard-<version>.tar.gz per release — docker load it, no registry pull needed. Put the dashboard behind TLS at your NEXT_PUBLIC_APP_URL.
The stock Web Store build can't reach your Supabase — its manifest only permits Medusa's SaaS host. Send Medusa your two URLs and we return a signed build that allow-lists your infra:
SELF_HOST_SUPABASE_URL = https://<your-ref>.supabase.co (or your domain) SELF_HOST_DASHBOARD_URL = https://dlp.your-company.com
Your build allows your Supabase + dashboard origins — so enrollment, policy, telemetry, approvals, SIEM, Realtime policy-push, and one-click enroll all work against your stack. The DLP model is bundled (no model hosting; air-gap friendly). You get three artifacts:
medusa-extension-selfhost.zip — unpacked build for a manual / trial install.medusa-extension-selfhost.crx + update.xml — a signed package + update manifest, which is what lets MDM force-installthe extension (Chrome won't force-install a zip).Source-licensed?
Build it yourself: node scripts/selfhost-up.mjs produces all of the above as part of one-command bring-up, or node scripts/build-selfhost.mjs alone — see SELF_HOSTING.md. The signing key selfhost-crx-key.pem is generated once; back it up — the Extension ID derives from it, so losing it orphans every MDM install.
The dashboard serves your .crx + update.xml at https://dlp.your-company.com/ext/ automatically (selfhost-up.mjs drops them into selfhost-stack/ext-dist/) — no static host or proxy route needed. Then roll out just like the SaaS flow — force-install + push the org key via Google Admin / Intune / Jamf, pointed at your update URL instead of the store:
build-selfhost/ (not the repo root — that targets the SaaS backend), or install the .crx. Open Options, paste an org key (Dashboard → Install, or Settings → API keys), and Connect. Any build can also be pointed at your dashboard by entering its URL in the Options Dashboard address field.<id>;…/ext/update.xml) + managed-config with the org_key so browsers auto-enroll silently.Enrolled browsers inherit your default policy automatically (Browsers → Default policy → Apply to every endpoint).
From the backend kit, against your live deployment:
node scripts/selfhost-smoke.mjs https://<ref>.supabase.co https://dlp.your-company.com
This confirms all 5 edge functions answer the CORS preflight and the dashboard's /api/health is up. Then do a real end-to-end test: enroll one browser, paste a fake secret (e.g. AKIAIOSFODNN7EXAMPLE or a test SSN) into ChatGPT, and confirm the finding appears in your self-hosted dashboard.
Both cloud and on-prem run the same all-in-one stack — what changes is the wrapper (host, DNS, TLS, firewall). The golden rule: SUPABASE_PUBLIC_URL and DASHBOARD_URL must be real hostnames that resolve the same from the browser and from inside the dashboard container — never localhost.
Verified end-to-end
This exact path was brought up on real Docker — all 12 containers healthy, migrations applied, all 5 edge functions + the dashboard health check green.
gcloud compute addresses create medusa-ip --region=us-central1 IP=$(gcloud compute addresses describe medusa-ip --region=us-central1 --format='value(address)') gcloud compute instances create medusa \ --zone=us-central1-a --machine-type=e2-standard-4 \ --image-family=ubuntu-2204-lts --image-project=ubuntu-os-cloud \ --boot-disk-size=60GB --address="$IP" --tags=medusa-web # expose only the TLS proxy; lock SSH to your IP gcloud compute firewall-rules create medusa-web --allow=tcp:80,tcp:443 \ --target-tags=medusa-web --source-ranges=0.0.0.0/0 gcloud compute firewall-rules create medusa-ssh --allow=tcp:22 \ --target-tags=medusa-web --source-ranges=<YOUR_IP>/32
dlp.your-company.com A <IP> supabase.your-company.com A <IP>
gcloud compute ssh medusa --zone=us-central1-a curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER && newgrp docker curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs git DASHBOARD_URL=https://dlp.your-company.com \ SUPABASE_PUBLIC_URL=https://supabase.your-company.com \ node scripts/selfhost-server.mjs cd selfhost-stack && docker compose up -d
Front it with Caddy (auto Let's Encrypt) so the two hostnames serve HTTPS, set an optional MEDUSA_LICENSE_KEY, roll out your per-org extension via MDM, then:
docker compose -f docker-compose.caddy.yml up -d # in selfhost-stack node scripts/selfhost-smoke.mjs https://supabase.your-company.com https://dlp.your-company.com
Hardening: snapshot the Postgres volume on a schedule; reach the VM via IAP; dedicated VPC; Cloud SQL for managed Postgres.
Same stack on a Linux host/VM inside your network (~4 vCPU / 8 GB / 60 GB; Docker + Node 20 + git). Works with zero outbound connectivity.
On a connected machine, then carry to the appliance:
docker/ folder into selfhost-stack/ (staging then skips the clone);docker load Medusa's medusa-dashboard-<version>.tar.gz (provided with your license); for the open-source Supabase services, docker save them on a connected host → docker load on the appliance;node_modules.dlp.corp.internal A <APPLIANCE_IP> supabase.corp.internal A <APPLIANCE_IP>
Issue certs for both hostnames from your internal CA (or Caddy with a DNS-01 challenge); push your CA root to clients if needed.
DASHBOARD_URL=https://dlp.corp.internal \ SUPABASE_PUBLIC_URL=https://supabase.corp.internal \ node scripts/selfhost-server.mjs cd selfhost-stack && docker compose up -d node scripts/selfhost-smoke.mjs https://supabase.corp.internal https://dlp.corp.internal
The MEDUSA_LICENSE_KEY is verified offline (Ed25519), so paid tiers work with no internet. Extension: MDM force-install your per-org build (or stock build + managed control_plane_url). Backups: the whole state is one Postgres volume — snapshot/restore it.
A self-hosted dashboard has no connection to Medusa's billing, so it reads a signed license key from MEDUSA_LICENSE_KEY and verifies it offline (Ed25519) — no phone-home, works air-gapped. Without a key it runs at the free tier.
| Tier | Endpoints | Retention | Notable features |
|---|---|---|---|
| Free | 3 | 30 days | Single user |
| Starter | 5 | 90 days | OAuth SSO, alerting |
| Pro | 25 | 1 year | RBAC, audit export, SIEM |
| Enterprise | Unlimited | Unlimited | SAML SSO, multi-tenant (MSSP) |
The active tier + license status show under Settings → License & Tier. To request or renew a key, contact sales@medusasec.com.
For a self-hosted deployment, upgrading is three steps:
# 1. apply new backend migrations/functions from the new release's kit tar -xzf medusa-selfhost-kit.tar.gz && cd medusa-selfhost-kit node scripts/selfhost-setup.mjs # 2. pull + restart the new dashboard image docker compose -f docker-compose.selfhost.yml pull docker compose -f docker-compose.selfhost.yml up -d # 3. if the extension changed, request a fresh per-org build and re-distribute # (your MDM auto-updates managed browsers)
On the SaaS path there's nothing to upgrade — the extension auto-updates from the Web Store and the dashboard is managed by Medusa.