# BemiDB
BemiDB is an open-source Snowflake and Fivetran alternative bundled together. It seamlessly connects to different data sources, syncs data in a compressed columnar format to S3, and allows you to run complex queries using its Postgres-compatible analytical query engine.

## Contents
- [Highlights](#highlights)
- [Use cases](#use-cases)
- [Quickstart](#quickstart)
- [Usage](#usage)
- [Syncing from Amplitude](#syncing-from-amplitude)
- [Syncing from Attio](#syncing-from-attio)
- [Syncing from Dialpad](#syncing-from-dialpad)
- [Syncing from Postgres](#syncing-from-postgres)
- [Customizing S3 endpoint](#customizing-s3-endpoint)
- [Configuration](#configuration)
- [Architecture](#architecture)
- [Benchmark](#benchmark)
- [Data type mapping](#data-type-mapping)
- [Roadmap](#roadmap)
- [License](#license)
## Highlights
- **Query Engine**: leverages a analytical query engine that is 2000x faster than regular Postgres.
- **Scalable Storage**: stores data in columnar format in object storage separated from compute.
- **Built-In Connectors**: automatically syncs data from different data sources.
- **Compressed Data**: uses an open table format with 4x data compression.
- **Easy Deployment**: packaged in a single Docker image with stateless processes.
- **Postgres Compatibility**: integrates with services and tools in the Postgres ecosystem.
- **Open-Source**: released under an OSI-approved license.
## Use cases
- **Centralize data without complex pipelines**. No complex setup and no weird acronyms like CDC or ETL.
- **Integrate with Postgres tools and services**. Querying data with BI tools, notebooks, and ORMs.
- **Run complex analytical queries at high speed**. Without worrying about performance impact or indexing.
- **Continuously archive data from your database**. Offloading and querying historical data.
## Quickstart
#### 1. Configure prerequisites for BemiDB:
- Set up a Postgres database as a data catalog for files stored in object storage:
```sql
CREATE USER catalog_user LOGIN PASSWORD 'password';
CREATE DATABASE catalog OWNER catalog_user;
```
- Create an S3 bucket and IAM user credentials with access to the bucket.
See AWS IAM policy example
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::[AWS_S3_BUCKET]",
"arn:aws:s3:::[AWS_S3_BUCKET]/*"
]
}
]
}
```
- Export the environment variables:
```sh
# Configured catalog database URL (host.docker.internal allows connecting to localhost from a container)
export CATALOG_DATABASE_URL=postgres://catalog_user:password@host.docker.internal:5432/catalog
# AWS S3 environment variables (data will be stored in s3://bemidb-bucket/iceberg/*)
export AWS_REGION=us-west-1
export AWS_S3_BUCKET=bemidb-bucket
export AWS_ACCESS_KEY_ID=[...]
export AWS_SECRET_ACCESS_KEY=[...]
```
#### 2. Sync data from a source Postgres database:
```sh
docker run \
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
-e DESTINATION_SCHEMA_NAME=postgres \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-postgres
```
#### 3. Start the BemiDB database server:
```sh
docker run \
-p 54321:54321 \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest server
```
#### 4. Query BemiDB with with a Postgres client:
```sh
# List all tables
psql postgres://localhost:54321/bemidb -c "SELECT table_schema, table_name FROM information_schema.tables"
# Query a table
psql postgres://localhost:54321/bemidb -c "SELECT COUNT(*) FROM postgres.[table_name]"
```
## Usage
#### Syncing from Amplitude
1. Create an [Amplitude API key](https://docs.gettelio.com/integrations/amplitude)
2. Run the syncer:
```sh
docker run \
-e SOURCE_AMPLITUDE_API_KEY=[...] \
-e SOURCE_AMPLITUDE_SECRET_KEY=[...] \
-e SOURCE_AMPLITUDE_START_DATE=2025-01-01 \
-e DESTINATION_SCHEMA_NAME=amplitude \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-amplitude
```
#### Syncing from Attio
1. Create an [Attio API access token](https://docs.gettelio.com/integrations/attio)
2. Run the syncer:
```sh
docker run \
-e SOURCE_ATTIO_API_ACCESS_TOKEN=[...] \
-e DESTINATION_SCHEMA_NAME=attio \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-attio
```
#### Syncing from Dialpad
1. Create a [Dialpad API key](https://docs.gettelio.com/integrations/dialpad)
2. Create a webhook endpoint:
```sh
curl -X POST "https://dialpad.com/api/v2/webhooks" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [DIALPAD_API_KEY]" \
-d '{
"hook_url": "https://[YOUR_DOMAIN]/[YOUR_WEBHOOK_ENDPOINT]",
"secret": "[YOUR_WEBHOOK_SECRET]"
}'
```
3. Subscribe to SMS events for the created webhook:
```sh
curl -X POST "https://dialpad.com/api/v2/subscriptions/sms" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [DIALPAD_API_KEY]" \
-d '{
"direction": "all",
"enabled": true,
"endpoint_id": "[WEBHOOK_ID]",
"include_internal": false,
"status": false
}'
```
4. Write a small service to receive Dialpad webhook events and publish them to NATS JetStream.
See example code in Node.js
```ts
import express from 'express';
import bodyParser from 'body-parser';
import { jwtVerify } from 'jose';
import { connect, JSONCodec } from 'nats';
const app = express();
app.use(bodyParser.json());
app.post('/dialpad-webhook', async (req, res) => {
const { payload } = await jwtVerify(request.body, new TextEncoder().encode('[YOUR_WEBHOOK_SECRET]'), { algorithms: ['HS256'] });
const jsonCodec = JSONCodec();
const natsConnection = await connect({ servers: "nats://host.docker.internal:4222" });
const jetstreamManager = await natsConnection.jetstreamManager();
await jetstreamManager.streams.add({ name: 'bemidb', subjects: ['bemidb.dialpad'] });
await jetstreamManager.jetstream().publish('bemidb.dialpad', jsonCodec.encode(payload));
});
app.listen(3000, () => console.log('Server is running on port 3000'));
```
5. Run the syncer:
```sh
docker run \
-e NATS_URL=nats://host.docker.internal:4222 \
-e NATS_JETSTREAM_STREAM=bemidb \
-e NATS_JETSTREAM_SUBJECT=bemidb.dialpad \
-e NATS_JETSTREAM_CONSUMER_NAME=bemidb-dialpad \
-e DESTINATION_SCHEMA_NAME=dialpad \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-dialpad
```
#### Syncing from Postgres
By default, BemiDB syncs all tables from the Postgres database. To include and sync only specific tables from your Postgres database:
```sh
docker run \
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
-e SOURCE_POSTGRES_INCLUDE_TABLES=public.table1,public.table2 \ # A comma-separated list of tables to include
-e DESTINATION_SCHEMA_NAME=postgres \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-postgres
```
To exclude specific tables during the sync:
```sh
docker run \
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
-e SOURCE_POSTGRES_EXCLUDE_TABLES=public.audit_log,public.cache \ # A comma-separated list of tables to exclude
-e DESTINATION_SCHEMA_NAME=postgres \
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
ghcr.io/bemihq/bemidb:latest syncer-postgres
```
#### Customizing S3 endpoint
BemiDB can work with various S3-compatible object storage solutions, such as MinIO.
You can run MinIO locally:
```sh
minio server ./minio-data
# API: http://172.17.0.3:9000 http://127.0.0.1:9000
# WebUI: http://172.17.0.3:9001 http://127.0.0.1:9001
```
Create a bucket named `bemidb-bucket` in MinIO:
```sh
mc alias set local http://localhost:9000 minioadmin minioadmin123
mc mb local/bemidb-bucket --ignore-existing
```
Export and use environment variables when starting BemiDB:
```sh
export AWS_REGION=us-west-1
export AWS_S3_BUCKET=bemidb-bucket
export AWS_ACCESS_KEY_ID=minioadmin
export AWS_SECRET_ACCESS_KEY=minioadmin123
export AWS_S3_ENDPOINT=http://localhost:9000
```
## Configuration
#### `syncer-amplitude` command options
| Environment variable | Default value | Description |
|-------------------------------|---------------|--------------------------------------------------------------------|
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
| `SOURCE_AMPLITUDE_API_KEY` | Required | Amplitude API key for authentication. |
| `SOURCE_AMPLITUDE_SECRET_KEY` | Required | Amplitude secret key for authentication. |
| `SOURCE_AMPLITUDE_START_DATE` | `2025-01-01` | Start date for syncing data from Amplitude in `YYYY-MM-DD` format. |
#### `syncer-attio` command options
| Environment variable | Default value | Description |
|---------------------------------|---------------|--------------------------------------------|
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
| `SOURCE_ATTIO_API_ACCESS_TOKEN` | Required | Attio API access token for authentication. |
#### `syncer-dialpad` command options
| Environment variable | Default value | Description |
|--------------------------------|---------------|----------------------------------------------------------------|
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
| `NATS_URL` | Required | NATS server URL for connecting to receive Dialpad SMS records. |
| `NATS_JETSTREAM_STREAM` | Required | NATS JetStream stream name. |
| `NATS_JETSTREAM_SUBJECT` | Required | NATS JetStream subject name. |
| `NATS_JETSTREAM_CONSUMER_NAME` | Required | NATS JetStream consumer name. |
| `NATS_FETCH_TIMEOUT_SECONDS` | `30` | Timeout in seconds for fetching messages from NATS. |
#### `syncer-postgres` command options
| Environment variable | Default value | Description |
|-------------------------------------|---------------|----------------------------------------------------------------------|
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
| `SOURCE_POSTGRES_DATABASE_URL` | Required | Postgres database URL to sync data from. |
| `SOURCE_POSTGRES_INCLUDE_TABLES` | | List of tables to include in sync. Comma-separated `schema.table`. |
| `SOURCE_POSTGRES_EXCLUDE_TABLES` | | List of tables to exclude from sync. Comma-separated `schema.table`. |
#### `server` command options
| Environment variable | Default value | Description |
|----------------------|---------------|----------------------------------------|
| `BEMIDB_HOST` | `0.0.0.0` | Host for BemiDB to listen on |
| `BEMIDB_PORT` | `54321` | Port for BemiDB to listen on |
| `BEMIDB_DATABASE` | `bemidb` | Database name |
| `BEMIDB_USER` | | Database user. Allows any if empty |
| `BEMIDB_PASSWORD` | | Database password. Allows any if empty |
#### Common options
| Environment variable | Default value | Description |
|--------------------------------------|--------------------|------------------------------------------------------|
| `CATALOG_DATABASE_URL` | Required | Postgres database URL for the catalog |
| `AWS_REGION` | Required | AWS region |
| `AWS_S3_BUCKET` | Required | AWS S3 bucket name |
| `AWS_ACCESS_KEY_ID` | Required | AWS access key ID |
| `AWS_SECRET_ACCESS_KEY` | Required | AWS secret access key |
| `AWS_S3_ENDPOINT` | `s3.amazonaws.com` | Custom S3 endpoint URL |
| `BEMIDB_LOG_LEVEL` | `INFO` | Log level: `ERROR`, `WARN`, `INFO`, `DEBUG`, `TRACE` |
| `BEMIDB_DISABLE_ANONYMOUS_ANALYTICS` | `false` | Disable collection of anonymous usage metadata |
## Architecture
BemiDB consists of the following main components packaged in a single Docker image:
- **Database Server**: implements the [Postgres protocol](https://www.postgresql.org/docs/current/protocol.html) to enable Postgres compatibility.
- **Query Engine**: embeds the [DuckDB](https://duckdb.org/) query engine to run analytical queries.
- **Storage Layer**: uses the [Iceberg](https://iceberg.apache.org/) open table format to store data in columnar compressed Parquet files.
- **Data Syncers**: to connect to different data sources and sync data to the storage layer.
## Benchmark
BemiDB is optimized for analytical workloads and can run complex queries up to 2000x faster than regular Postgres.
On the TPC-H benchmark with 22 sequential queries, BemiDB outperforms Postgres by a significant margin:
* Scale factor: 0.1
* BemiDB unindexed: 2.3s 👍
* Postgres unindexed: 1h23m13s 👎 (2,170x slower)
* Postgres indexed: 1.5s 👍 (99.97% bottleneck reduction)
* Scale factor: 1.0
* BemiDB unindexed: 25.6s 👍
* Postgres unindexed: ∞ 👎 (infinitely slower)
* Postgres indexed: 1h34m40s 👎 (220x slower)
See the [benchmark](/benchmark) directory for more details.
## Data type mapping
Primitive data types are mapped as follows:
| PostgreSQL | Parquet | Iceberg |
|-------------------------------------------------------------|-----------------------------------------------------------------|----------------------------------|
| `bool` | `BOOLEAN` | `boolean` |
| `bit`, `int2`, `int4` | `INT32` | `int` |
| `int8`, | `FIXED_LEN_BYTE_ARRAY(9)` (`DECIMAL(38, 0)` / `DECIMAL(38, 0)`) | `decimal(38, 0)` |
| `xid` | `INT64` | `long` |
| `xid8`, `interval` | `FIXED_LEN_BYTE_ARRAY(9)` (`DECIMAL(38, 6)` / `DECIMAL(38, 6)`) | `decimal(38, 6)` |
| `float4` | `FLOAT` | `float` |
| `float8` | `DOUBLE` | `double` |
| `numeric` | `FIXED_LEN_BYTE_ARRAY(16)` (`DECIMAL(P, S)` / `DECIMAL(P, S)`) | `decimal(P, S)` |
| `date` | `INT32` (`DATE` / `DATE`) | `date` |
| `time`, `timetz` | `INT64` (`TIME_MICROS`) | `time` |
| `timestamp`, `timestamptz` | `INT64` (`TIMESTAMP_MICROS`) | `timestamp` |
| `varchar`, `text`, `bpchar` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
| `uuid` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
| `bytea` | `BYTE_ARRAY` | `binary` |
| `point`, `line`, `lseg`, `box`, `path`, `polygon`, `circle` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
| `cidr`, `inet`, `macaddr`, `macaddr8` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
| `tsvector`, `xml`, `pg_snapshot` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
| `json`, `jsonb` | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` (JSON logical type) |
| `_*` (array) | `LIST` `*` | `list` |
| `*` (user-defined type) | `BYTE_ARRAY` (`STRING` / `UTF8`) | `string` |
Note that Postgres `json` and `jsonb` types are implemented as JSON logical types and stored as strings (Parquet and Iceberg don't support unstructured data types).
You can query JSON columns using standard operators, for example:
```sql
SELECT * FROM [TABLE] WHERE [JSON_COLUMN]->>'[JSON_KEY]' = '[JSON_VALUE]';
```
## Roadmap
- [x] Postgres protocol and query support
- [x] Iceberg write operations
- [x] Selective data syncing from Postgres
- [x] Postgres compatibility with other tools
- [x] psql
- [x] Metabase
- [x] TablePlus
- [x] DBeaver
- [x] pgAdmin
- [x] Grafana
- [x] Retool
- [ ] Jupyter notebooks ([#27](https://github.com/BemiHQ/BemiDB/issues/27))
- [x] Data syncing from other sources
- [x] Amplitude (incremental)
- [x] Attio CRM (full-refresh)
- [x] Postgres (full-refresh)
- [x] Dialpad (real-time)
- [ ] HubSpot
- [ ] Stripe
- [ ] Google Sheets
- [ ] MySQL
- [ ] SQLite ([#24](https://github.com/BemiHQ/BemiDB/issues/24))
- [x] Iceberg tables compaction
- [x] Packaging in a Docker image
- [x] Table compaction without Trino as a dependency
- [x] Materialized views
- [x] Transformations with dbt ([#25](https://github.com/BemiHQ/BemiDB/issues/25))
- [ ] Partitioned tables ([#15](https://github.com/BemiHQ/BemiDB/issues/15))
Are you looking for real-time data syncing? Check out [BemiDB Cloud](https://bemidb.com), our managed data platform.
## License
Distributed under the terms of the [AGPL-3.0 License](/LICENSE). If you need to modify and distribute the code, please release it to contribute back to the open-source community.