← Back to Documentation

Deployment Guide

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.

Which path is for you

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.

Org-wide rollout (SaaS)

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:

  1. Force-install the extension on managed browsers.
  2. Push the org key so each browser auto-enrolls with no user interaction.

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.

Step 1 — Generate the org key

1

Create an org enrollment key

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.

Step 2 — Chrome via Google Admin

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.)

Step 2 — Windows (Intune / GPO)

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.

Step 2 — macOS (Jamf / MDM)

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.

Step 2 — Microsoft Edge

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.

Step 3 — Verify the fleet

As managed browsers come online they appear on the Browsers page. From there:

  • Set policy per endpoint, or roll a default to all (Browsers → Default policy → Apply to every endpoint).
  • Findings and approvals flow in automatically — no text ever leaves the browser, only metadata.
  • Confirm a real detection: on a managed browser, paste a fake secret (e.g. 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.

Self-hosting

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.

How we serve a closed-source image

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).

Fastest — one command (installer image)

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.

What you need (from source)

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:

  • Docker + Docker Compose — runs everything: your PostgreSQL database, auth, the edge functions, and the dashboard.
  • Node 20+ — to run the one-command bring-up script.
  • The backend kit (medusa-selfhost-kit.tar.gz) — issued on purchase.
  • A Medusa license key (optional) — for any tier above free; verified offline.

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.

Step 1 — Stand up the whole stack (one command)

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:

  • stands up the open-source Supabase stack in Docker (Postgres + auth + APIs) — your database, on your box;
  • generates every secret (incl. POLICY_SIGNING_SECRET) and applies all DB migrations (tables, RLS, org foundation, SIEM, approvals);
  • deploys the 5 edge functions — extension-register, gateway-policy, gateway-events, dlp-approval-request, dlp-approval-status;
  • starts the dashboard, and smoke-tests the whole thing.

“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).

Alternative — bring your own Supabase / Postgres

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+.

1

Apply migrations + deploy edge functions to your Supabase

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.

2

Run just the dashboard image

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.

Step 2 — Get your per-org extension

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).
  • a deterministic Extension ID (printed by the build) — paste it into Install → Org-wide rollout.

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.

Step 3 — Roll out + enroll endpoints

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:

  • Manual / trial: load 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.
  • Org-wide (MDM): Dashboard → Install → Org-wide rollout, toggle Self-hosted and paste your Extension ID — it emits the force-install entry (<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).

Step 4 — Verify

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.

Deploy on GCP (single VM, ~30 min)

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.

1

Static IP + VM + firewall

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
2

DNS → the static IP

dlp.your-company.com       A  <IP>
supabase.your-company.com  A  <IP>
3

Install Docker + Node, then bring up the stack

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
4

TLS, license, extension, verify

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.

Deploy on-prem (air-gapped)

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.

1

(Air-gapped) pre-stage the images

On a connected machine, then carry to the appliance:

  • copy Supabase's 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;
  • carry over the kit + node_modules.
2

Internal DNS + TLS

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.

3

Bring up + verify

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.

Licensing & tiers

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.

TierEndpointsRetentionNotable features
Free330 daysSingle user
Starter590 daysOAuth SSO, alerting
Pro251 yearRBAC, audit export, SIEM
EnterpriseUnlimitedUnlimitedSAML SSO, multi-tenant (MSSP)

The active tier + license status show under Settings → License & Tier. To request or renew a key, contact sales@medusasec.com.

Upgrading

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.