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

<Note>
  This page is for whoever administers your database. Setting up the sync needs
  someone who can create a database user and knows your host, port and TLS
  setup. If that is not you, send them this page: the rest of the site does not
  depend on it.
</Note>

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 → Directory">
    In the dashboard sidebar, open **Integrations**, then the **Directory** tab
    (on an empty workspace, **Browse integrations** takes you there; once you
    have a connection, **Add integration** does). Pick the **PostgreSQL** or
    **MySQL** card: 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="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="Save">
    Click **Save**. Boom tests the connection as it saves: it opens a short,
    read-only connection over TLS and reads the database version. The
    connection lands on its own page with an **Active** badge if the test
    passed, or **Error** (with a plain-language message) if it didn't. A failed
    connection is still saved, so you can fix the details with **Edit** and
    retest with **Test connection** on that page. See
    [Troubleshooting](#troubleshooting).
  </Step>
</Steps>

<Note>
  Every connection uses TLS (SSL mode **Require**: encrypted, server
  certificate not verified), which suits managed databases such as Neon, RDS
  and Cloud SQL. The form has no SSL setting. If you need the server
  certificate verified against your own CA, contact
  [support](mailto:support@useboom.ai) to set it up. See
  [Current limitations](#current-limitations).
</Note>

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

Boom switches syncing on for your connection once it's saved (ask
[support](mailto:support@useboom.ai) if you don't see it yet). It then appears
under **Integrations → Syncs**. Open it and click **+ Add data** to build a
source in four steps: **What it becomes**, **Choose a table**,
**Query & mapping**, and **Preview & go live**.

### 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: choose what the data becomes (**People**,
**Objects** or **Link**), then pick a table Boom discovered and click **Use
this table**. Boom writes a starter `SELECT` for you, which you can refine and
check with **Run preview** before **Create sync**.

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

A new source starts syncing when you click **Go live**. Use **Pause** on a
source to stop 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. Boom always connects over TLS, so
    the database must accept TLS connections. Check that SSL is enabled on the
    server and that nothing between Boom and the database strips it.
  </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.
* **No SSL setting in the form.** Every connection uses **Require** (encrypted,
  certificate not verified). Contact support if you need the server
  certificate verified against your own CA.
* **Syncing is switched on by Boom.** Saving a connection doesn't create its
  sync yet; Boom enables it, after which you build sources yourself from
  **Integrations → Syncs**.

## 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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.