# metabase-iac

Infrastructure as Code for [Metabase](https://www.metabase.com). Manage collections, questions (SQL, MongoDB, structured), dashboards, and text cards as code.

Similar to how Grafana dashboards can be managed as JSON, `metabase-iac` lets you version-control your Metabase resources and apply changes through a CLI.

## Install

```bash
pnpm add metabase-iac
# or
npm install metabase-iac
```

## Quick start

```bash
# 1. Create a new directory for your Metabase instance
mkdir metabase-prod && cd metabase-prod
pnpm init && pnpm add metabase-iac

# 2. Configure credentials
cat > .env <<EOF
METABASE_URL=https://your-metabase.example.com
METABASE_USERNAME=admin@example.com
METABASE_PASSWORD=your-password
EOF

# 3. Pull all existing resources from Metabase
npx metabase-iac pull

# 4. You now have collections/, questions/, dashboards/ — commit them to git
git init && git add -A && git commit -m "Initial pull from Metabase"
```

## Commands

| Command | Description |
|---|---|
| `metabase-iac pull` | Import collections, questions, and dashboards from Metabase |
| `metabase-iac plan` | Preview what changes would be applied (dry run) |
| `metabase-iac apply` | Push local resource definitions to Metabase |
| `metabase-iac databases` | List connected databases |
| `metabase-iac collections` | List collections |

### Options

```
pull
  -o, --output <dir>         Output directory (default: ".")
  --import-base <path>       Import path for types in generated files (default: "metabase-iac/types")

plan / apply
  -d, --dir <dir>            Resources directory (default: ".")
```

## Authentication

Configure via environment variables or a `.env` file:

```env
METABASE_URL=https://your-metabase.example.com

# Option 1: Username + password
METABASE_USERNAME=admin@example.com
METABASE_PASSWORD=your-password

# Option 2: Session token
METABASE_SESSION_TOKEN=your-session-token

# Option 3: API key
METABASE_API_KEY=mb_xxxx
```

## Resource types

### Collections

```typescript
import { collection } from 'metabase-iac/types'

export default collection({
  id: 23,
  slug: 'engineering',
  name: 'Engineering',
  description: 'Engineering team dashboards',
  parent: 'Teams', // optional — omit for root
})
```

### Native queries (SQL)

Two files: a `.sql` with the query and a `.meta.json` with metadata.

```sql
-- questions/daily-signups.sql
SELECT date_trunc('day', created_at) AS day, count(*)
FROM users
GROUP BY 1
ORDER BY 1 DESC
LIMIT 30
```

```json
{
  "id": 42,
  "name": "Daily Signups",
  "collection": "Engineering",
  "database": "production",
  "display": "line",
  "description": "Daily user signups over the last 30 days"
}
```

### Native queries (MongoDB)

Same pattern, with `.mongodb` extension:

```json
[
  { "$match": { "status": "active" } },
  { "$group": { "_id": "$region", "count": { "$sum": 1 } } },
  { "$sort": { "count": -1 } }
]
```

### Structured queries (MBQL)

```typescript
import { structuredQuestion } from 'metabase-iac/types'

export default structuredQuestion({
  id: 37,
  name: 'accounts-count',
  collection: 'Engineering',
  database: 'production',
  display: 'scalar',
  datasetQuery: {
    stages: [{ aggregation: [["count", {}]], "source-table": 77 }],
    database: 2,
  },
})
```

### Dashboards

```typescript
import { dashboard } from 'metabase-iac/types'

export default dashboard({
  id: 3,
  name: 'Engineering Overview',
  collection: 'Engineering',
  cards: [
    { question: 'Daily Signups', questionId: 42, width: 12, height: 6, row: 0, col: 0 },
    { question: 'accounts-count', questionId: 37, width: 6, height: 3, row: 0, col: 12 },
    { text: '## Notes\nUpdated daily.', width: 24, height: 2, row: 6, col: 0 },
  ],
})
```

## Workflow

```
pull → review diff → edit → plan → apply → commit
```

1. `metabase-iac pull` — import current state from Metabase
2. Review changes with `git diff`
3. Edit `.ts`/`.sql`/`.mongodb` files as needed
4. `metabase-iac plan` — preview what will change
5. `metabase-iac apply` — push to Metabase
6. `git commit` — version control your changes

## Project structure

A consumer repo looks like this:

```
my-metabase/
├── collections/
│   └── *.ts
├── questions/
│   ├── *.ts              # Structured (MBQL) queries
│   ├── *.sql             # Native SQL queries
│   ├── *.mongodb          # Native MongoDB queries
│   └── *.meta.json       # Metadata for native queries
├── dashboards/
│   └── *.ts
├── .env                  # Credentials (gitignored)
├── .gitignore
├── package.json
└── tsconfig.json         # Optional — for IDE type checking
```

## How it works

- **Pull** reads from the Metabase REST API and generates typed resource files
- **Plan/Apply** loads local resource files, diffs against a state file (`.metabase-state.json`), and creates/updates resources via the API
- Resources are matched by name within their collection
- Ordering is automatic: collections are created before questions, questions before dashboards
- Both SQL and MongoDB databases are fully supported — native queries are stored as plain text files

## License

MIT
