> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useboom.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect your database

> Sync customers into the Boom CDP straight from your own PostgreSQL or MySQL database.

Boom can read customers directly from your operational database and keep them
in sync with the CDP — no CSV exports, no custom ETL. You point Boom at a
read-only user on your database, write a `SELECT` that returns the records you
care about, and map the columns to people, custom objects, and relationships.

This guide walks an admin through the whole setup:

<CardGroup cols={2}>
  <Card title="1. Create a read-only user" icon="user-lock" href="#step-1-create-a-read-only-user">
    A least-privilege database user Boom connects as.
  </Card>

  <Card title="2. Connect your database" icon="plug" href="#step-2-connect-your-database">
    Enter host, credentials, and TLS — then test the connection.
  </Card>

  <Card title="3. Define what to sync" icon="table-columns" href="#step-3-define-what-to-sync">
    Write the query and map columns to customer data.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="#troubleshooting">
    Decode the most common connection errors.
  </Card>
</CardGroup>

<Note>
  You connect through the Boom dashboard, not the API. Open
  [app.useboom.ai](https://app.useboom.ai) and go to **Integrations** in the
  sidebar. The whole flow needs an admin who can create a database user and
  knows the database's host, port, and TLS setup.
</Note>

## Before you begin

<CardGroup cols={2}>
  <Card title="A supported database" icon="database">
    **PostgreSQL** or **MySQL**. Connecting and testing works for both today;
    automated syncing currently runs for **PostgreSQL** (see
    [Current limitations](#current-limitations)).
  </Card>

  <Card title="A read-only user" icon="user-lock">
    Boom only ever reads. Create a dedicated user with `SELECT`-only grants —
    [snippets below](#step-1-create-a-read-only-user).
  </Card>

  <Card title="Network reachability" icon="network-wired">
    Boom must be able to reach the database — either a **publicly reachable
    host**, or a **SSH bastion** (jump host) in front of a private database.
  </Card>

  <Card title="TLS" icon="lock">
    The connection is encrypted in transit. `Require` is the default; use
    `Verify full` to also validate the server certificate against a CA.
  </Card>
</CardGroup>

<Info>
  **What Boom stores and how.** Boom stores the connection details and
  credentials encrypted at rest (AES-256-GCM). The password is never returned
  to the browser after you save it, and credentials never appear in logs. Boom
  opens short-lived, read-only connections — it does not write to your database.
</Info>

## Step 1 — Create a read-only user

Give Boom a dedicated user with read-only access to the tables you want to
sync. This keeps the blast radius small and makes the connection easy to audit
and revoke. The same snippets are available in-product, next to the connection
form.

<CodeGroup>
  ```sql PostgreSQL theme={null}
  CREATE USER boom_readonly WITH PASSWORD 'your-strong-password';
  GRANT CONNECT ON DATABASE your_db_name TO boom_readonly;
  GRANT USAGE ON SCHEMA public TO boom_readonly;
  GRANT SELECT ON ALL TABLES IN SCHEMA public TO boom_readonly;
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO boom_readonly;
  ```

  ```sql MySQL theme={null}
  CREATE USER 'boom_readonly'@'%' IDENTIFIED BY 'your-strong-password';
  GRANT SELECT ON your_db_name.* TO 'boom_readonly'@'%';
  FLUSH PRIVILEGES;
  ```
</CodeGroup>

<Tip>
  Replace `your_db_name` and `your-strong-password` with real values. The
  final `ALTER DEFAULT PRIVILEGES` line (PostgreSQL) makes sure Boom can also
  read tables created **after** you grant access — handy when your schema grows.
  To scope Boom to specific tables instead of the whole schema, grant `SELECT`
  on just those tables.
</Tip>

## Step 2 — Connect your database

<Steps>
  <Step title="Open Integrations → Add integration">
    In the dashboard sidebar, open **Integrations**, then **Add integration**.
    Pick **PostgreSQL** or **MySQL** — the default port fills in automatically
    (`5432` for PostgreSQL, `3306` for MySQL).
  </Step>

  <Step title="Fill in the connection details">
    | Field             | What to enter                                                                                                                                                                            |
    | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | **Name**          | A label you'll recognize later, e.g. `Production Postgres`.                                                                                                                              |
    | **Database type** | PostgreSQL or MySQL.                                                                                                                                                                     |
    | **Host**          | The database's DNS name or public IP, e.g. `db.example.com`. Must be publicly reachable in **Direct** mode (see [SSH tunnel](#connecting-through-an-ssh-bastion) for private databases). |
    | **Port**          | Defaults to the database's standard port; change it if yours differs.                                                                                                                    |
    | **Database name** | The database to connect to, e.g. `acme_production`.                                                                                                                                      |
    | **Username**      | The read-only user from [Step 1](#step-1-create-a-read-only-user).                                                                                                                       |
    | **Password**      | The user's password. Stored encrypted; never shown again after saving.                                                                                                                   |
  </Step>

  <Step title="Choose an SSL mode">
    Under **Security**, pick how strictly Boom verifies the server's
    certificate. TLS is always used — this controls verification:

    | Mode            | Behavior                                                                                  | When to use                                                      |
    | --------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
    | **Require**     | TLS required; the server certificate is **not** verified.                                 | Default. Good for most managed databases (Neon, RDS, Cloud SQL). |
    | **Verify full** | TLS required **and** the server certificate verified against a CA certificate you supply. | Highest assurance — see the note below.                          |
    | **Disable**     | No TLS.                                                                                   | **Not recommended** — only for local development.                |

    <Warning>
      Avoid **Disable** for any database holding real customer data — the
      connection would be unencrypted in transit.
    </Warning>

    <Note>
      **Verify full** requires a CA certificate, and uploading one isn't
      available in the connection form yet. Use **Require** — which still
      encrypts the connection — unless you specifically need certificate
      verification; if you do, contact [support](mailto:support@useboom.ai) to
      set it up. See [Current limitations](#current-limitations).
    </Note>
  </Step>

  <Step title="Pick a connection method">
    **Direct** (default) connects straight to the host you entered. If your
    database is locked inside a private network, choose **SSH Tunnel (bastion)**
    instead — see [below](#connecting-through-an-ssh-bastion).
  </Step>

  <Step title="Test the connection">
    Click **Test connection**. Boom opens a short, read-only connection, checks
    TLS, and reads the database version. On success you'll see the verified
    version (e.g. *Connection verified · PostgreSQL 16.2*). On failure you get a
    plain-language message and a hint — see [Troubleshooting](#troubleshooting).
  </Step>

  <Step title="Save">
    Click **Save**. Boom re-runs the test automatically as it saves, so the
    connection lands with the right status without a separate click — an
    **Active** badge if it passed, or **Error** (with the message) if it didn't.
    A failed connection is still saved, so you can fix the details and retest
    from its page.
  </Step>
</Steps>

### Connecting through an SSH bastion

If your database has no public endpoint, Boom can reach it through an **SSH
bastion** (a jump host that *is* publicly reachable and can connect onward to
the database). Choose **SSH Tunnel (bastion)** as the connection method and fill
in the bastion fields:

| Field                | What to enter                                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| **Bastion host**     | The jump host's public address, e.g. `bastion.example.com`.                                                  |
| **Bastion port**     | SSH port, usually `22`.                                                                                      |
| **Bastion username** | The SSH user on the bastion.                                                                                 |
| **SSH private key**  | A private key Boom authenticates with. Use a **dedicated, least-privilege key** that can only forward ports. |
| **Key passphrase**   | Optional — only if your key is passphrase-protected.                                                         |
| **Pinned host key**  | Optional — paste the bastion's `SHA256:…` fingerprint to pin it, or pin it after the first successful test.  |

In tunnel mode you still enter the **database** host and port as the bastion
sees them (often a private address like `10.0.x.x` or an internal DNS name) —
Boom dials the bastion, then forwards onward to the database.

<Note>
  If the test fails with a *could not reach the database host/port* error, the
  bastion is likely refusing TCP port forwarding, or it can't reach the
  database from its own network. Confirm the bastion's SSH server allows
  forwarding — set `AllowTcpForwarding yes` in `/etc/ssh/sshd_config` and reload
  `sshd` — and that the bastion can itself connect to the database host and port.
</Note>

<Tip>
  **Host-key pinning** protects against a man-in-the-middle on the bastion.
  Leave it blank to trust the key on first connect, then pin the fingerprint
  Boom shows you. Once pinned, a changed key is refused until you re-pin it.
</Tip>

## Step 3 — Define what to sync

A connection on its own doesn't move any data — it just proves Boom can reach
the database. To actually sync, you add one or more **data sources**. A source
is a `SELECT` query plus a mapping that tells Boom how to turn each row into a
person, a custom object, or a relationship.

Open the **Data sources** area for your connection to build them.

### Resource kinds

Each source produces one **kind** of record:

| Kind              | Use it for                                                        | Lands in                                                            |
| ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- |
| **Person**        | Humans — customers, leads, users.                                 | People, keyed by your `external_id`, with email/phone for outreach. |
| **Custom object** | Everything else — orders, loans, products, subscriptions.         | Typed custom objects.                                               |
| **Relationship**  | A link between two records (typically a many-to-many join table). | An edge only — no record of its own.                                |

The fastest way to start: pick a table Boom discovered and choose **Draft as
person** or **Draft as custom object**. Boom writes a starter `SELECT` for you,
which you can then refine.

### Required columns

Every source query **must** project three columns, whatever else it returns.
They're how Boom keeps the sync correct and incremental:

| Column              | Purpose                                                                                                                                                              |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `external_id`       | A stable, unique id for the row (usually the primary key). Boom uses it verbatim to match rows on every sync, so the same record updates instead of duplicating.     |
| `source_updated_at` | A last-modified timestamp. Boom remembers the latest value it has seen and, on each run, pulls only rows changed since then — that's what makes ongoing syncs cheap. |
| `source_deleted`    | A boolean. When `true`, Boom marks the synced record as removed while keeping its last-known data.                                                                   |

```sql theme={null}
SELECT
  id                       AS external_id,
  updated_at               AS source_updated_at,
  (deleted_at IS NOT NULL) AS source_deleted,
  email,
  phone,
  plan_tier,
  signup_date
FROM public.users;
```

<Warning>
  If you project `source_deleted` from a soft-delete column, **don't also filter
  those rows out** (e.g. `WHERE deleted_at IS NULL`) — the deleted rows would
  never reach Boom, so `source_deleted` would always be `false` and deletions
  would never sync. Either include them (as above) so Boom sees the flag flip,
  or, if you only ever want live rows, drop `source_deleted` from the projection
  and let Boom's daily reconciliation detect removals.
</Warning>

<Note>
  Boom runs your query in a **read-only transaction** and previews only a small
  number of rows while you're editing, so it's safe to iterate. Make sure
  `external_id` is unique per row — for a join table without a single-column
  key, project one (e.g. `left_id || ':' || right_id AS external_id`).
</Note>

### Map your columns

Once the query runs, map the returned columns:

<Steps>
  <Step title="Identity (Person sources)">
    Map at least one of **email** or **phone** so Boom can reach the customer.
    These populate the person's contact channels, which is what lets
    event-triggered journeys message them.
  </Step>

  <Step title="Attributes">
    Map any other columns to **attributes** and give each a type (String,
    Number, Date, Boolean). Attributes become filterable fields in Segments, so
    the right type matters — a `Date` compares chronologically, a `Number`
    numerically.
  </Step>

  <Step title="Descriptions (optional but recommended)">
    Add a short description per column to build a data dictionary. **Describe
    with AI** can draft these for you — it only ever sees column **names and
    types**, never row values. Descriptions are saved to your attribute catalog
    and survive future syncs.
  </Step>
</Steps>

### Link records with relationships

Relationships connect a person to an object, or one object to another (a
customer who `placed` an order; an order that `has_line_item`). On a Person or
Custom object source, add a relationship by pointing a **foreign-key column** at
another source:

| Field               | Meaning                                                           |
| ------------------- | ----------------------------------------------------------------- |
| **FK column**       | The column on *this* row that holds the other record's id.        |
| **Kind**            | `person_to_co` (person → object) or `co_to_co` (object → object). |
| **Target resource** | The other source the id refers to.                                |
| **Role**            | A label for the link, e.g. `placed`, `has_line_item`.             |

For a **many-to-many** join table, add it as its own **Relationship** source.
Its query projects `left_external_id` and `right_external_id` (plus the three
required columns) and emits one edge per row — useful when the link itself
carries data (a quantity, a redeemed-at timestamp), which becomes the edge's
attributes.

<Tip>
  Sync the target of a relationship **before** the source that points at it
  (e.g. orders before line items). If an edge references a record Boom hasn't
  seen yet, Boom tolerates it and re-links automatically on a later run — so
  ordering is a convenience, not a hard requirement.
</Tip>

### How syncing runs

Once a source is **enabled**, Boom keeps it in sync automatically — roughly
every 15 minutes — using `source_updated_at` to pull only what changed since
the last run:

* **First sync** backfills all matching rows. Large tables backfill in batches
  across several runs; you don't need to do anything.
* **Ongoing syncs** are incremental and usually pull a handful of rows (or
  none).
* **Deletions** are reconciled by a daily check, and rows flagged with
  `source_deleted = true` are marked removed as they sync.

Use the **Enabled** toggle on a source to pause syncing without deleting your
mapping.

## Troubleshooting

When a test or sync fails, Boom shows a plain-language message. Here's what the
common ones mean.

<AccordionGroup>
  <Accordion title="Authentication failed. Check username and password.">
    The username or password is wrong, or the user isn't allowed to connect
    from Boom's network. Re-run the [Step 1](#step-1-create-a-read-only-user)
    grants, confirm the password, and (MySQL) make sure the user is created
    with a host pattern that allows remote connections, e.g. `'boom_readonly'@'%'`.
  </Accordion>

  <Accordion title="Authentication succeeded but the user lacks permission.">
    Boom connected but can't read a table. Grant `SELECT` on the tables (or
    schema) you're syncing — see the read-only snippets above.
  </Accordion>

  <Accordion title="Host not found. Check the hostname spelling.">
    The hostname didn't resolve. Verify the host, and that it's a public DNS
    name or IP (in Direct mode). For private databases, use an
    [SSH bastion](#connecting-through-an-ssh-bastion).
  </Accordion>

  <Accordion title="Connection timed out.">
    Boom reached the network but got no response in time — usually a firewall
    or security group blocking the port. Allow inbound access to the database
    port from Boom, or front the database with a bastion.
  </Accordion>

  <Accordion title="Connection refused.">
    The host is reachable but nothing is listening on that port. Check the port
    number and that the database is running and accepting TCP connections.
  </Accordion>

  <Accordion title="TLS handshake failed.">
    The encrypted handshake didn't complete. Check the **SSL mode** — if you
    chose **Verify full**, the server's certificate must be valid and match a
    trusted CA. Try **Require** to confirm the rest of the connection works.
  </Accordion>

  <Accordion title="Cannot connect to private, loopback, or link-local addresses.">
    In **Direct** mode Boom refuses private (`10.x`, `172.16–31.x`,
    `192.168.x`), loopback (`127.x`), and link-local (`169.254.x`) addresses
    for security. Use a publicly reachable host, or connect through an
    [SSH bastion](#connecting-through-an-ssh-bastion).
  </Accordion>

  <Accordion title="Database '…' does not exist on this server.">
    The **Database name** is wrong, or the read-only user can't see it. Confirm
    the exact name and that the user has `CONNECT` on it.
  </Accordion>

  <Accordion title="Could not reach the database host/port (SSH tunnel).">
    The bastion connected but couldn't forward to the database. Make sure the
    bastion allows TCP forwarding and can itself reach the database host and
    port. Double-check the database host/port as the **bastion** sees them.
  </Accordion>
</AccordionGroup>

## Current limitations

The database-source feature is live, with a few capabilities still on the way.
Plan around these:

* **No static egress IP to allowlist (yet).** Boom currently connects from a
  shared, variable IP range, so it can't give you a single fixed IP to add to
  your firewall. If your database can't be exposed publicly, use an
  [SSH bastion](#connecting-through-an-ssh-bastion). Static egress IPs are
  planned.
* **No AWS PrivateLink.** Private connectivity is via SSH bastion today;
  PrivateLink is not available yet.
* **No customer-managed keys (BYOK).** Credentials are encrypted with a
  Boom-managed key. Bring-your-own-key is not available yet.
* **Automated sync is PostgreSQL-first.** You can create and test **MySQL**
  connections today, but scheduled syncing currently runs for **PostgreSQL**
  sources. MySQL syncing is on the roadmap.
* **`Verify full` needs a CA certificate that the form can't yet accept.** The
  connection form doesn't expose a CA-certificate field, so `Verify full`
  isn't selectable end-to-end yet. Use **Require** (still encrypted) for now, or
  contact support if you need full certificate verification.

## Related

<CardGroup cols={2}>
  <Card title="Relationship types" icon="shapes" href="/relationship-types">
    Understand how links between records are modeled across the CDP.
  </Card>

  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Prefer to push data over the API? Start here instead.
  </Card>
</CardGroup>

<Note>
  Prefer to push data instead of pulling it? You can also send people, objects,
  events, and relationships over the [CDP API](/introduction) — the two
  approaches coexist.
</Note>
