# Serve your data from your own network

Keep your map fresh without uploading exports by hand, and without giving
Maplith a password to any of your systems. You run Maplith Bridge, a small,
read-only server, next to your data. Maplith reads the files it serves, on the
schedule you set, and you can cut it off at any moment.

> **Maplith holds no database credential.** You run the Bridge, inside your
> own network, and you decide which files it serves. The only thing Maplith
> holds is the token for it, which you create and can replace.

## What the Bridge does

The Bridge is `maplith bridge`, a command of the npm package `maplith`
(the file `bridge.js`; `maplith-bridge` is the same command under its
older name), with the server (`serve.js`) and the PostgreSQL extractor
(`extract-postgres.js`) beside it. It runs on Node.js 24 or newer, and serving needs nothing else
installed. The files are short enough to read in full before you run them.
`node bridge.js serve` serves one folder of files over MCP, which is exactly
what Maplith's connector reads:

- **Read-only.** It lists and reads files, as MCP resources and as two tools,
  `list_files` and `read_file`, which run the same checks. Nothing in it
  writes, and it accepts no file.
- **One folder, nothing outside it.** A request can never reach a file outside
  the folder, not through `..` in a name and not through a symbolic link.
- **Only data files.** It serves CSV, TSV, JSON, JSONL, Markdown and plain
  text. Hidden files and folders (a name starting with a dot) are never listed
  or read.
- **Each top-level folder is a system.** `crm/customers.csv` and
  `crm/deals.csv` become the system "crm" in your map, `shop/orders.csv`
  the system "shop". Files lying loose at the top become one more system,
  named after the server.
- **Files that fit.** Maplith reads at most 8 MB from one resource, so the
  server lists a file only when it fits. Split a larger table into several
  files, or cut it to a recent period.
- **A token on every request.** Every request for data needs your bearer
  token, which the Bridge reads from a file only its owner can read. The right
  token is always served. After 10 wrong tokens from one address within 10
  minutes, wrong tokens from it get `429` for 15 minutes, and the Bridge logs
  it. A request with no token at all is refused and not counted.
  `GET /healthz` answers `204` with no body and needs no token, for a health
  check.
- **This machine only, unless you say otherwise.** It listens on 127.0.0.1 by
  default, and it refuses any other address without HTTPS: with a certificate
  of its own, or behind a reverse proxy you name (step 4). You can also limit
  the addresses that may call.

Any MCP client you give the token to can use the two tools too. A file's
content is data, never instructions, and the Bridge tells every client so.

The Bridge is open source under Apache 2.0, at
[github.com/Maplith/agents](https://github.com/Maplith/agents). Version
1.2.0-rc.1 is a release candidate. Beside the code: a
[runbook](https://github.com/Maplith/agents/blob/main/RUNBOOK.md) for your IT
lead, the [threat model](https://github.com/Maplith/agents/blob/main/THREAT-MODEL.md),
which names the test behind each defence, and a
[security policy](https://github.com/Maplith/agents/blob/main/SECURITY.md) that
says how to report a vulnerability.

The Bridge never calls home: it sends no telemetry and checks for no updates.
Security fixes are published as advisories on the repository. To get security
notices and release news by email instead, see
[maplith.com/bridge/updates](https://maplith.com/bridge/updates): it is opt-in,
with your own address, and every email has a link to stop.

## Get the Bridge

Each release is built by the repository's own release workflow and published
in two places that must agree: the
[releases page](https://github.com/Maplith/agents/releases), as
`maplith-1.2.0-rc.1.tgz` with its SHA-256 checksum and a signed record
of the build (an attestation), and npm, as the package `maplith` with
provenance. (Until 1.2.0-rc.1 the package was called `maplith-bridge`; that
name is deprecated on npm and points here.)

**Try it.** On any machine with Node.js 24 or newer, npm fetches the package
and runs it:

```sh
npx --yes maplith@1.2.0-rc.1 bridge --help
```

Always name the exact version. Every command on this page runs as
`node bridge.js` in the folder where the Bridge is installed; with npx, write
`npx --yes maplith@1.2.0-rc.1 bridge` in its place.

**On a server, verify it first.** Download the release, its checksum and its
attestation, and check all three: the checksum; that npm holds the same file
(the two `sha512-` lines must be equal); and, with the GitHub CLI
(`gh`), that the file was built from the repository by its release
workflow.

```sh
VERSION=1.2.0-rc.1
BASE=https://github.com/Maplith/agents/releases/download/v$VERSION
curl -fLO $BASE/maplith-$VERSION.tgz
curl -fLO $BASE/maplith-$VERSION.tgz.sha256
curl -fLO $BASE/maplith-$VERSION.tgz.sigstore.json
sha256sum -c maplith-$VERSION.tgz.sha256
echo "sha512-$(openssl dgst -sha512 -binary maplith-$VERSION.tgz | openssl base64 -A)"
npm view maplith@$VERSION dist.integrity
gh attestation verify maplith-$VERSION.tgz --repo Maplith/agents \
  --bundle maplith-$VERSION.tgz.sigstore.json
```

Releases before 1.1.0 were built when the repository was called
`Maplith/bridge`, so their attestations name that repository; this one, like
every release from 1.1.0, names `Maplith/agents`.

Then unpack it into one folder, for example `/opt/maplith-bridge`, and install
the PostgreSQL client the extractor uses:

```sh
mkdir -p /opt/maplith-bridge && tar -xzf maplith-1.2.0-rc.1.tgz -C /opt/maplith-bridge --strip-components=1
cd /opt/maplith-bridge
npm ci --ignore-scripts --omit=dev
node bridge.js --version
```

`npm ci` installs one package, `pg`, pinned to the exact version Maplith
tested (pg 8.23.1), and the packages it needs. The package's
`npm-shrinkwrap.json` pins every file it installs by its checksum, and no
install script runs. Success prints a line that starts with `added`, such as
`added 14 packages`. Serving needs nothing but Node.js.

The [runbook](https://github.com/Maplith/agents/blob/main/RUNBOOK.md) takes it
from here on a Linux server, in about an hour: two system users, so the
process the network reaches never holds the database password, the read-only
role, a nightly extract, and a service behind HTTPS. The steps below say the
same for each part.

**Or let your own agent install it.** Claude Code, or any agent that can run
commands on the server, can follow the runbook for you:
[Install with your agent](https://github.com/Maplith/agents/blob/main/INSTALL-WITH-YOUR-AGENT.md)
has a prompt to paste. The agent verifies the download in both places before it
reads anything from it, stops for you at every decision and every `sudo`, and
never asks for, sees or prints a password or the Bridge's token: where a step
needs one, it gives you the command to run in your own terminal.

## 1. Put the files in a folder

Maplith reads only what you put in the folder, so what goes in is your choice.
Use one subfolder per system, with the files your systems' own export buttons
or jobs produce. The same recent period from every system gives the best map:
a few months is enough.

By default a scan reads one source at a time. Put every system you want read
together in this folder. Where combined scanning is available, a workspace admin
can enable **One map from your sources** under **Settings › Data & retention** after queued
and running scans finish. Each later scan reads all current connections and
its new upload; earlier uploads keep their map without their files being read
again. The switch is one way. Data sources › Scans shows which sources were read,
partly read, failed, held or removed, with file counts. A failed source keeps
its previous map; it is not treated as an empty export. A connection request
can join a waiting combined scan without starting another one. If that upload
fails, the joined connection request is queued without the upload. On Data sources › Scans,
a combined map can only be restored from another combined scan.

Leave out what must not be processed. Maplith tries to mask personal data
before anything reaches the model, but that is best effort: you decide what
the folder contains.

**Keeping it fresh.** Refresh the folder on a schedule of your own, for example
a nightly cron job, so it is complete before Maplith's scheduled scan (from
05:30 Copenhagen time). Write each file under a name starting with a dot and
rename it when it is complete: the server never serves a hidden file, so a scan
never reads half a file.

## 2. From a database: a read-only role on the tables you choose

If the data lives in a PostgreSQL database, fill the folder under a role that
can read only the tables you choose: with Maplith's extractor, which you run in
your own network next to the server, or with an export job of your own. Either
way, the password stays with you: Maplith never holds it.

Run this as a database owner or administrator, with your own names in place of
the angle brackets:

```sql
-- A login role for the export, and nothing else.
CREATE ROLE maplith_export LOGIN PASSWORD '<a long random password>'
  NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;

-- Every session of the role is read-only, as a second safeguard.
-- The real guarantee is that the role is granted nothing that writes.
ALTER ROLE maplith_export SET default_transaction_read_only = on;

-- Access to the database and to the schema that holds the tables.
GRANT CONNECT ON DATABASE <your_database> TO maplith_export;
GRANT USAGE ON SCHEMA <your_schema> TO maplith_export;

-- Read access to the tables you choose, one by one.
GRANT SELECT ON <your_schema>.<table_one>, <your_schema>.<table_two> TO maplith_export;

-- Or only some columns of a table: leave out what must not be read.
GRANT SELECT (<column_a>, <column_b>, <column_c>) ON <your_schema>.<table_three> TO maplith_export;
```

Then check what the role can read, and that it can write nothing:

```sql
-- Every column the role can read: only the tables and columns you chose.
SELECT table_schema, table_name, column_name, privilege_type
FROM information_schema.column_privileges
WHERE grantee = 'maplith_export'
ORDER BY 1, 2, 3, 4;

-- Anything but reading: this should return no rows.
SELECT table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'maplith_export' AND privilege_type <> 'SELECT';
```

Every row of the first answer should say `SELECT`, on a column you chose.

> **Do not give the role a blanket read role.** `pg_read_all_data` reads
> every table, view and sequence in the database, including the ones you add
> next year. The same goes for `pg_read_server_files`, `pg_monitor` and
> `pg_read_all_stats`, for superuser, for `BYPASSRLS` (which ignores row
> security), for
> `GRANT SELECT ON ALL TABLES IN SCHEMA`, and for
> `ALTER DEFAULT PRIVILEGES … GRANT SELECT`, which grants every future table
> without anyone deciding. Grant table by table, so a new table is read only
> when you add it.

### Run the extractor

The extractor is one more file in the Bridge, `extract-postgres.js`, that
you run with `node bridge.js extract postgres`.
It is short enough to read before you run it. It connects as
`maplith_export` and writes one CSV file per table into the folder the
server serves.

- **It refuses a role with any privilege on its list.** Before it reads a
  row, it checks the role and every role it is a member of. It stops, and
  names what it found, if the role:
  - is a superuser, may create roles or databases, bypasses row security, or
    may replicate;
  - is a member of a built-in role such as `pg_read_all_data`;
  - may change any table, view, column, sequence, large object, schema or
    database, or create objects in one;
  - may pass any of its access on: a grant option (`WITH GRANT OPTION`) on
    anything, or the admin option on a group (`WITH ADMIN OPTION`);
  - owns anything: a table, function, type, large object, schema, database,
    or another catalog object, such as a publication for replication or an
    extension;
  - may run a function with its owner's rights (`SECURITY DEFINER`), by a
    grant to the role or a group it is in;
  - may use a foreign-data wrapper or a foreign server;
  - on PostgreSQL 15 or newer, may change a server parameter: a grant of
    `SET` or `ALTER SYSTEM` on it.

  It also stops when large-object access checks are switched off
  (`lo_compat_privileges`), and when the session logged in as another role
  and switched to this one. That is the list it checks; together with the
  read-only transaction below, it is what the extractor promises, not a proof
  that PostgreSQL offers no other way to write.
- **It runs only its own statements.** It reads in one read-only transaction,
  with a time limit on every statement and a deadline for the run. It takes no
  SQL from you: what it reads is what you granted.
- **Your password stays on your machine.** The extractor hands it to the
  PostgreSQL client and to nothing else. It never prints it, never writes it
  to a file, and connects to your database and nowhere else.
- **What it reads.** The tables, views and materialized views the role can
  read, and only the columns you granted. Foreign tables are left out, and
  named in its output: a read there would run on another server. Each becomes
  `<schema>/<table>.csv`, so each schema is a system in your map. With
  `--system <name>`, every table goes to `<name>/<schema>.<table>.csv`
  instead, as one system.
- **The same files every time.** The rows are written in primary-key order,
  or in the order of every column when a table has none, so two runs on the
  same data write the same CSV files. A table is cut at 50,000 rows
  (`--max-rows`), or sooner if the file would pass the 8 MB the server
  serves. A cut file opens with one line that says so; for a table with a
  single column, the cut is recorded in `.maplith-extract.json` instead.
- **Each file appears whole.** It is written under a hidden name and renamed
  when complete. Beside the files, the hidden `.maplith-extract.json` records
  what was read: the columns and their types, the declared keys, the
  catalog's estimate of each table's rows, how many values are empty or
  distinct, and which tables were cut. It holds counts and names, never a
  value, and the server never serves it. The estimates come from PostgreSQL's
  sampled statistics, so they can change after your database runs `ANALYZE`
  even when the data has not; the CSV files do not.
- **What it removes.** A table you stop granting has its file removed on the
  next run. It removes only files it wrote itself, never others. A file of
  your own at the same path as a table's file, for example a hand export of
  that table, is overwritten.

**What you need:** Node.js 24 or newer (`node --version` prints `v24` or
higher), and the Bridge installed as in [Get the Bridge](#get-the-bridge),
with `npm ci` run once in its folder: the extractor needs the PostgreSQL
client it installs. Commands below run in that folder.

**Set the connection.** Run the extractor as its own system user, not the
one that runs the server, so the password never sits next to the process the
internet reaches. Give that user the password in a `~/.pgpass` file only it
can read (`chmod 600`), typed at a prompt that does not echo it, so it is in
no command line, environment variable or script. A `:` or `\` in the
password is written `\:` or `\\`:

```sh
bash -c 'umask 077; read -rs -p "Password: " pw; echo;
  printf "%s:%s:%s:%s:%s\n" "<host>" 5432 "<your_database>" maplith_export "$pw" > ~/.pgpass'
export PGHOST=<host> PGPORT=5432 PGDATABASE=<your_database>
export PGUSER=maplith_export PGSSLMODE=verify-full
```

The PostgreSQL client reads `~/.pgpass` itself. `PGPASSWORD` and
`DATABASE_URL` work too, but they put the password in the environment.

**TLS.** The extractor connects with a verified certificate to any database
that is not on the same machine, unless you choose a mode yourself with
`PGSSLMODE` or a parameter in `DATABASE_URL`. A `DATABASE_URL` with no
host follows `PGHOST`. Choose `verify-full`. In
the PostgreSQL client the extractor uses, `sslmode=no-verify`, and
`uselibpqcompat=true` together with `sslmode=require` or `prefer`, turn
certificate checking off.

**The files** it writes are readable by their owner and group only (`0640`),
and the folders it creates `0750`. Give the folder to the extractor's user,
with the server's user as its group, and `chmod 2750` it, so the server can
read the files and nobody else can.

**Check the role.** `--check` connects, runs every check and lists the tables
it would read, and writes nothing:

```sh
node bridge.js extract postgres --out /path/to/exports --check
```

Success lists one line per table and ends with
`OK: the role is read-only and can read <n> tables. Nothing was written.`
A table you expected and do not see is not granted to `maplith_export`.

**Run it.**

```sh
node bridge.js extract postgres --out /path/to/exports
```

Success prints one line per file, such as `shop/orders.csv: 1200 rows`, and
ends with `OK: wrote <n> files to /path/to/exports.` The exit code is 0. Any
failure prints one message and exits with 1. Every file it finished is whole;
tables it did not reach keep the file from the last good run, so the folder can
hold parts of two runs until the next run succeeds.

**Keep it fresh.** Schedule the run, for example nightly with cron, so it
finishes before Maplith's scheduled scan (from 05:30 Copenhagen time):

```sh
# crontab -e, with the variables above set in the job's environment
0 3 * * * cd /opt/maplith-bridge && node bridge.js extract postgres --out /path/to/exports
```

The options:

| Option | What it does |
|---|---|
| `--out <folder>` or `EXPORT_DIR` | The folder the server serves. Required. |
| `--system <name>` | Put every table under one system folder. |
| `--max-rows <n>` | Rows per table at most. Default 50,000. |
| `--statement-timeout <seconds>` | The limit on each statement. Default 300. |
| `--deadline <minutes>` | The limit on the whole run. Default 60. |
| `--check` | Check the role and list the tables; write nothing. |

**If it refuses.** The message starts with `Refusing to run` and lists each
thing it found, one per line. Fix each one as a database owner or
administrator, then run `--check` again. `<role>` and `<object>` are the
names in the message. When `<role>` is not `maplith_export`, it is a role
`maplith_export` is a member of: revoke the privilege from that role, or end
the membership with `REVOKE <role> FROM maplith_export;`.

| The message says | Do this |
|---|---|
| `<role> has the attribute SUPERUSER` (or CREATEROLE, CREATEDB, BYPASSRLS, REPLICATION) | `ALTER ROLE <role> NOSUPERUSER;` (or `NOCREATEROLE`, `NOCREATEDB`, `NOBYPASSRLS`, `NOREPLICATION`). For a superuser, create the separate `maplith_export` role from step 2 instead. |
| `the role is a member of pg_…` | `REVOKE pg_… FROM maplith_export;` |
| `<role> may change <schema>.<table>` | `REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER ON <schema>.<table> FROM <role>;` On PostgreSQL 17 or newer, add `MAINTAIN` to the list. If the grant was made to `PUBLIC`, revoke it from `PUBLIC`. |
| `<role> may advance or set the sequence <object>` | `REVOKE USAGE, UPDATE ON SEQUENCE <object> FROM <role>;` |
| `<role> owns <object>` | Give the object another owner, for example `ALTER TABLE <object> OWNER TO <another_role>;`. A login role for the extractor should own nothing. |
| `<role> may create objects in, or owns, the schema <object>` | `REVOKE CREATE ON SCHEMA <object> FROM <role>;` If it is `public` on PostgreSQL 14 or older: `REVOKE CREATE ON SCHEMA public FROM PUBLIC;` |
| `<role> may create schemas in, or owns, the database <object>` | `REVOKE CREATE ON DATABASE <object> FROM <role>;` |
| `<role> may write the large object <oid>` | `REVOKE UPDATE ON LARGE OBJECT <oid> FROM <role>;` If the grant was made to `PUBLIC`, revoke it from `PUBLIC`. |
| `lo_compat_privileges is on` | As a superuser: `ALTER SYSTEM SET lo_compat_privileges = off; SELECT pg_reload_conf();` (or `ALTER ROLE <role> RESET lo_compat_privileges;` where it was set for one role). |
| `<role> may run the function <object> with its owner's rights` | `REVOKE EXECUTE ON FUNCTION <object> FROM <role>;` |
| `<role> may grant <privilege> on <object> to other roles (WITH GRANT OPTION)` | `REVOKE GRANT OPTION FOR <privilege> ON <object> FROM <role>;` The privilege itself stays. |
| `<role> may add other roles to <group> (WITH ADMIN OPTION)` | `REVOKE ADMIN OPTION FOR <group> FROM <role>;` |
| `<role> may change the server parameter <object>` | `REVOKE SET, ALTER SYSTEM ON PARAMETER <object> FROM <role>;` |
| `<role> may use the foreign-data wrapper <object>` (or `the foreign server`) | `REVOKE USAGE ON FOREIGN DATA WRAPPER <object> FROM <role>;` (or `REVOKE USAGE ON FOREIGN SERVER <object> FROM <role>;`) |
| `the session logged in as … and switched to …` | Connect with `PGUSER=maplith_export` itself, not as another user who then switches role. |
| `the transaction is not read-only` | Write to [support@maplith.com](mailto:support@maplith.com) with the message. |

**If it warns.** A line that starts with `Warning: every role in this
database` names functions that run with their owner's rights
(`SECURITY DEFINER`), which every role may call because
PostgreSQL grants `EXECUTE` on a function to `PUBLIC` unless it was
revoked. Such a function can write, whatever its volatility, so the
extractor names them. It still runs: it never calls them, and its transaction
is read-only, so a write inside one would fail. It is a property of the whole
database rather than of `maplith_export`, so there is no grant to that role
to take back. If no application needs to call a function through
`PUBLIC`, close it for every role:
`REVOKE EXECUTE ON FUNCTION <object> FROM PUBLIC;`, then grant it to the
roles that call it.

**Other messages.**

| The message says | Do this |
|---|---|
| `The pg package did not load` | Run `npm ci --ignore-scripts --omit=dev` in the folder that holds the files. |
| `password authentication failed` | Check `PGUSER` and `PGPASSWORD`. The password in the message, if any, is shown as `[redacted]`. |
| A certificate error, such as `self-signed certificate` | Your database's certificate is not trusted by Node.js. Point `NODE_EXTRA_CA_CERTS` at your certificate authority's file. Do not turn verification off. |
| `Stopped: the run passed its deadline` | Raise `--deadline`, or grant fewer tables. |
| `canceling statement due to statement timeout` | A table took longer than `--statement-timeout` to read. Raise it, or lower `--max-rows`. |

### Or export by hand

Export each table to a CSV file in its system's folder. With `psql`, for
example, which writes the file on your side:

```sh
psql "host=<host> dbname=<your_database> user=maplith_export sslmode=verify-full" \
  -c "\copy (SELECT <column_a>, <column_b>, <column_c> FROM <your_schema>.<table_three>) TO 'exports/<system>/.<table_three>.csv' WITH (FORMAT csv, HEADER)"
mv exports/<system>/.<table_three>.csv exports/<system>/<table_three>.csv
```

Name the columns rather than `SELECT *` where you granted only some of them.

## 3. Run the server

Create the token. The Bridge writes a long random one to a file only its owner
can read, and prints where it is, never the token itself:

```sh
node bridge.js rotate-token --token-file /etc/maplith-bridge/token
```

Then start the server on the folder:

```sh
node bridge.js serve --dir /path/to/exports --token-file /etc/maplith-bridge/token
```

It prints its address, where the token is and the steps to connect it in
Maplith. It never prints the token: copy it from the file when you connect.
It refuses a token file other users can read, one inside the folder it
serves, one that is not a regular file of at most 1 KB, and any token but
one `rotate-token` wrote: 64 random lowercase hexadecimal characters, with
patterned strings refused. Do not write a token by hand. It checks the file
again each time it changes.

The server listens on `127.0.0.1:8787` by default, so it is not reachable
from outside until you serve it over HTTPS (step 4). `node bridge.js serve --help`
lists every option.

## 4. Serve it over HTTPS

Maplith's connector reads only over HTTPS, and only from a public address: it
refuses a name that resolves to a private network. The Bridge refuses to
listen beyond this machine in plain HTTP, so choose one of two:

- **HTTPS in the Bridge.** Give it a certificate and its key, and a public
  address:
  `node bridge.js serve --dir /path/to/exports --token-file /etc/maplith-bridge/token --host 0.0.0.0 --port 8443 --tls-cert cert.pem --tls-key key.pem`.
  Its address is then `https://<your-host>:8443/mcp`. Restart it after you
  renew the certificate.
- **A reverse proxy in front**, such as Caddy or nginx, or your platform's
  HTTPS edge. Name the proxy's address with `--behind-proxy`, for example
  `--behind-proxy 127.0.0.1` for a proxy on the same machine: the Bridge then
  takes requests from the proxy only, and counts failed token attempts by the
  caller address the proxy forwards. A proxy on another host is named by its
  addresses, in ranges no wider than /12 for IPv4 or /29 for IPv6, and an
  IPv4 proxy by its IPv4 address.
  `--behind-proxy any` is accepted only with `--host 127.0.0.1`, for a
  proxy on the same machine.

`--allow <addresses>` serves only callers from those IP addresses or ranges.
Maplith does not publish fixed addresses it calls from yet, so use it for your
own MCP clients, not to name Maplith. The token is the gate: a request without
it gets `401`.

Check it before you connect it, and never copy the token to another machine
to do so. From outside your network, without the token, the answer must be
`401`: that proves the name, the certificate and the route.

```sh
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://<your-host>/mcp \
  -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
```

On the Bridge's own host, read the token from its file with a shell builtin,
so it appears in no command line:

```sh
printf 'Authorization: Bearer %s\n' "$(< /etc/maplith-bridge/token)" | \
  curl -s -X POST http://127.0.0.1:8787/mcp -H @- -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"resources/list"}'
```

The answer lists your files as `exports://<system>/<file>`. With the token
from outside, **Test connection** in Maplith is the check (step 5).

## 5. Connect it in Maplith

A workspace admin or builder does this, in the app:

1. Open Data sources › Systems and choose **Add MCP connection**.
2. Give it a label, paste the HTTPS URL as **MCP URL**, and type the token
   from the token file as **Bearer token**. Type the token in the app only,
   never into a chat with an assistant. Maplith encrypts it at rest, never
   shows it again and never gives it to an agent.
3. **Test connection** shows how many files the server lists.
4. **Review the scan** shows the systems and files Maplith recognised, and
   **Start scan** builds the map. Follow it in Data sources › Scans.
5. To keep the map fresh, set **Rebuild this map** under **Settings › Data & retention** to every
   day or every week. Maplith then reads the folder again and reports what
   changed as signals.

## What Maplith reads, and what it keeps

- **Only what the server lists.** Maplith reads the files in your folder
  through the server, and nothing else: not your database, not other folders.
- **Files pass through for a build.** The worker reads them, builds the map
  and deletes them after the scan, exactly as it does with an upload.
- **The map is metadata.** It holds structure, not rows: objects, fields, how
  your systems join and how well each join was measured, findings. A finding
  can quote evidence and a field description can quote an example value; see
  [Security & access model](/docs/security) for what can appear and how
  masking works.
- **Nothing is left out in silence.** Data sources › Scans names any file the
  connector could not read, and why.

## Revoke access

You stay in control, from either side:

- **On your side, at once:** run `node bridge.js rotate-token` with your token
  file, or stop the server. A running Bridge refuses the old token from its
  next request, with no restart, and from that moment Maplith can read
  nothing. To let Maplith read again, remove the connection under  Data sources › Systems and add it again with the new token. Maplith checks its connections
  twice a day, and then raises a `link_dead` signal, so your agents know the map has stopped
  following the business.
- **In Maplith:** a workspace admin chooses **Remove** on the connection under  Data sources › Systems. The token is deleted with it.
- **Less data:** remove a file or a system's folder, and the next scan no
  longer reads it.
- **The database role:** `DROP OWNED BY maplith_export; DROP ROLE maplith_export;`
  removes its grants and the role (run the first part in each database you
  granted access in).