Back to Blog
Evidence.devAnalytics EngineeringSQLData AppsDuckDBdbtBIData VisualizationStatic SitesOpen Source

Evidence.dev — Code-Driven Analytics Reports and Interactive Data Apps

A practical guide to Evidence.dev: the SQL-in-Markdown framework that compiles analytics reports to fully static sites with no backend required at runtime, bootstrapping a project with create-evidence and the dev server that rebuilds pages in milliseconds on file save, project layout with pages/ for .md report files and sources/ for connector configs, the evidence.config.yaml showQueryEditor and sidebar settings, connector packages for DuckDB, BigQuery, Snowflake, PostgreSQL, Databricks, Trino, and CSV with credential injection via environment variables, DuckDB initSQL for creating views over S3 Parquet files with hive partitioning, named SQL code blocks in Markdown that run at build time and expose typed result arrays as named variables on each page, built-in BarChart, LineChart, AreaChart, Heatmap, FunnelChart, BigValue, and DataTable components with data props, fmt format strings, and series configuration, Column components inside DataTable with colorscale and inline component content types, SvelteKit-style dynamic route files like [customer_id].md that generate one page per query row with params.customer_id interpolation for drill-down reports, Dropdown and DateRange Inputs components that filter query results client-side without a backend via inputs variable binding in WHERE clauses, dbt integration pattern where dbt materializes mart tables and Evidence queries them directly in a monorepo, GitHub Actions CI/CD pipeline running dbt first then Evidence build with credentials from secrets and Cloudflare Pages deployment, custom Svelte components in the components/ directory with typed props and npm-installed libraries for extended charting, Dockerfile with multi-stage build and Nginx serving Evidence static output with HTTP basic auth and aggressive cache headers for hashed assets, Kubernetes CronJob pattern for nightly Evidence rebuilds with S3 sync and CloudFront invalidation, and a 10-point production checklist covering credential management, CI validation of report queries, named query conventions, showQueryEditor in production, dynamic page cardinality limits, .evidence/ build cache in CI, connector version pinning, format string usage, last-updated metadata display, and accessibility validation.

2026-06-23

The Problem with Traditional BI

Business intelligence dashboards built in Tableau, Power BI, or Looker solve the discovery problem for non-technical users but create a maintenance nightmare for data engineers. Every report is a GUI artifact: a collection of drag-and-drop configurations stored in a proprietary format that cannot be version-controlled, code-reviewed, or tested. When a metric definition changes, someone has to manually find every report that references it and update each one by hand. When a dashboard breaks, the error is a blank chart — no stack trace, no line number, no test failure.

Evidence.dev takes the opposite approach: analytics reports as code. You write SQL queries and Markdown in .md files, embed built-in chart and table components, and Evidence compiles the whole thing into a fast, self-contained static website. Reports live in Git, get reviewed in pull requests, and deploy through CI/CD pipelines exactly like application code. Combined with dbt for transformation logic, Evidence gives analytics engineers a fully code-driven stack from raw source to published report — no GUI required at any layer.

SQL-First

Queries are plain SQL in fenced code blocks. Evidence executes them at build time against your data source and passes results to chart components as typed arrays.

Static Output

Build output is a fully static site — no backend, no database at runtime. Data is baked into the HTML/JS bundle. Serve it from S3, Cloudflare Pages, or Nginx.

Version Control

Every report, query, and metric definition is a text file in Git. PRs show exactly which SQL changed. CI validates that reports build before merge.

Installation and Project Structure

Evidence requires Node.js 18+ and npm. The create-evidence scaffolder creates a minimal project with a dev server, a sample report, and a DuckDB connector pre-configured. The dev server watches .md files and rebuilds pages on save, giving a browser preview within a few hundred milliseconds of every edit.

# Bootstrap a new Evidence project
npm create evidence@latest my-analytics

cd my-analytics
npm install
npm run dev
# → http://localhost:3000

# Project layout after scaffolding:
my-analytics/
├── pages/                        # One .md file = one report page
│   ├── index.md                  # Homepage (required)
│   ├── revenue/
│   │   ├── overview.md
│   │   └── by-channel.md
│   └── operations/
│       └── pipeline-health.md
├── sources/                      # Data source connection configs
│   ├── warehouse/
│   │   └── connection.yaml       # BigQuery / Snowflake / DuckDB connection
│   └── local_files/
│       └── connection.yaml       # DuckDB reading local Parquet/CSV
├── components/                   # Custom Svelte components (optional)
│   └── KpiCard.svelte
├── static/                       # Static assets (images, logos)
├── evidence.config.yaml          # Project-level config
└── package.json
# evidence.config.yaml
# Controls layout, interactivity, and deployment settings

title: "Analytics Platform"
description: "Internal data reports for DataOps team"

# Show or hide the query editor in the rendered report
# Set to false for production; true for analyst self-service
showQueryEditor: false

# Controls which pages appear in the sidebar navigation
# 'auto' discovers all .md files; 'manual' reads from evidence.sidebar.yaml
sidebar: auto

# Deployment target for 'npm run build'
# 'static' = plain HTML/JS/CSS; 'netlify', 'cloudflare', 'github-pages'
deployment:
  target: static
  basePath: /reports  # if serving from a sub-path

Note

Evidence uses SvelteKit under the hood. The .md files are processed by a custom Markdown plugin that extracts SQL code blocks, executes them against the configured data source at build time, and injects the results as typed JavaScript objects available to the component expressions on that page.

Data Source Connectors

Evidence supports a growing list of connectors installed as npm packages. The default connector is DuckDB, which lets you query local Parquet, CSV, and JSON files without any external database. For production deployments against a cloud warehouse, you install the relevant connector package and add credentials to sources/. DuckDB's columnar engine makes Evidence extremely fast for local analytics and prototyping — a 10GB Parquet file queries in seconds without loading it into memory.

# Install connector packages for your warehouse
npm install @evidence-dev/bigquery
npm install @evidence-dev/snowflake
npm install @evidence-dev/postgres
npm install @evidence-dev/databricks
npm install @evidence-dev/duckdb        # default, usually pre-installed
npm install @evidence-dev/mysql
npm install @evidence-dev/trino
npm install @evidence-dev/csv           # read CSV files via DuckDB

# sources/warehouse/connection.yaml — BigQuery connector
connector: bigquery
project_id: my-gcp-project
# Credentials from EVIDENCE_BIGQUERY_CREDENTIALS env var (base64 JSON service account)
# or from application default credentials (gcloud auth application-default login)

---

# sources/warehouse/connection.yaml — Snowflake connector
connector: snowflake
account: myorg-myaccount
database: ANALYTICS
warehouse: COMPUTE_WH
schema: DBT_PROD
# Credentials from env: EVIDENCE_SNOWFLAKE_USERNAME, EVIDENCE_SNOWFLAKE_PASSWORD

---

# sources/local_files/connection.yaml — DuckDB reading Parquet from S3
connector: duckdb
# DuckDB SQL executed before any query to configure extensions and secrets
initSQL: |
  INSTALL httpfs;
  LOAD httpfs;
  SET s3_region = 'eu-west-1';
  CREATE SECRET aws_s3 (
    TYPE S3,
    PROVIDER CREDENTIAL_CHAIN
  );
# sources/local_files/connection.yaml — multiple file sources via DuckDB
connector: duckdb
initSQL: |
  -- Create views over Parquet files exported from dbt
  CREATE OR REPLACE VIEW orders AS
    SELECT * FROM read_parquet('data/orders_*.parquet');

  CREATE OR REPLACE VIEW customers AS
    SELECT * FROM read_parquet('data/customers.parquet');

  CREATE OR REPLACE VIEW events AS
    SELECT * FROM read_parquet('data/events/*.parquet', hive_partitioning=true);

Writing Your First Report

An Evidence page is a Markdown file with SQL code blocks and component tags. The SQL blocks run at build time and make their result available as a named variable. Component tags like <BarChart> and <DataTable> take those variables as props. Between components you write regular Markdown prose — headings, paragraphs, callouts. The result looks like a technical document with live data embedded in it, not a traditional dashboard.

---
title: Revenue Overview
description: Daily and monthly revenue trends with channel breakdown
---

# Revenue Overview

This report covers the last 90 days of order revenue across all acquisition channels.
Data refreshes daily at 06:00 UTC from the `fct_orders` dbt model.

```sql daily_revenue
-- Named 'daily_revenue' — available as {daily_revenue} on this page
SELECT
    ordered_at::date                        AS date,
    acquisition_channel,
    SUM(gross_revenue_usd)                  AS gross_revenue,
    SUM(net_revenue_usd)                    AS net_revenue,
    COUNT(DISTINCT order_id)                AS order_count,
    COUNT(DISTINCT customer_id)             AS unique_customers,
    ROUND(SUM(gross_revenue_usd) / NULLIF(COUNT(DISTINCT order_id), 0), 2) AS revenue_per_order
FROM warehouse.fct_orders
WHERE ordered_at >= CURRENT_DATE - INTERVAL 90 DAY
GROUP BY 1, 2
ORDER BY 1 DESC, 3 DESC
```

```sql summary_kpis
SELECT
    SUM(gross_revenue_usd)                         AS total_revenue,
    COUNT(DISTINCT order_id)                       AS total_orders,
    COUNT(DISTINCT customer_id)                    AS unique_customers,
    ROUND(AVG(gross_revenue_usd), 2)               AS avg_order_value,
    SUM(gross_revenue_usd) - LAG(SUM(gross_revenue_usd))
        OVER (ORDER BY DATE_TRUNC('month', ordered_at))    AS mom_change
FROM warehouse.fct_orders
WHERE ordered_at >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL 1 MONTH)
```

## Key Metrics (Last 30 Days)

<BigValue
  data={summary_kpis}
  value="total_revenue"
  title="Gross Revenue"
  fmt="usd0"
/>

<BigValue
  data={summary_kpis}
  value="total_orders"
  title="Orders"
  fmt="num0"
/>

<BigValue
  data={summary_kpis}
  value="avg_order_value"
  title="Avg Order Value"
  fmt="usd2"
/>

## Daily Revenue by Channel

<BarChart
  data={daily_revenue}
  x="date"
  y="gross_revenue"
  series="acquisition_channel"
  type="stacked"
  title="Gross Revenue by Acquisition Channel"
  yFmt="usd0k"
/>

## Detail Table

<DataTable
  data={daily_revenue}
  rows={25}
  search={true}
  sortable={true}
>
  <Column id="date" title="Date" fmt="date" />
  <Column id="acquisition_channel" title="Channel" />
  <Column id="gross_revenue" title="Gross Revenue" fmt="usd0" />
  <Column id="order_count" title="Orders" fmt="num0" />
  <Column id="revenue_per_order" title="Rev / Order" fmt="usd2" />
</DataTable>

Built-In Chart and Table Components

Evidence ships with over 25 built-in components covering the full BI component palette: bar charts, line charts, area charts, scatter plots, bubble charts, heatmaps, maps, funnel charts, sankey diagrams, KPI tiles, data tables with inline sparklines, and more. All components accept a data prop pointing to a named query result, column name props specifying which fields map to axes and series, and format strings for value display.

```sql funnel_data
SELECT
    stage,
    stage_order,
    COUNT(DISTINCT user_id) AS users,
    ROUND(100.0 * COUNT(DISTINCT user_id) /
        FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (ORDER BY stage_order), 1) AS conversion_pct
FROM warehouse.user_funnel_events
WHERE event_date >= CURRENT_DATE - INTERVAL 30 DAY
GROUP BY 1, 2
ORDER BY 2
```

<FunnelChart
  data={funnel_data}
  nameCol="stage"
  valueCol="users"
  title="30-Day Conversion Funnel"
/>

---

```sql cohort_retention
SELECT
    cohort_month,
    months_since_signup,
    ROUND(100.0 * retained_users / cohort_size, 1) AS retention_rate
FROM warehouse.cohort_retention
WHERE cohort_month >= '2025-01-01'
```

<Heatmap
  data={cohort_retention}
  x="months_since_signup"
  y="cohort_month"
  value="retention_rate"
  title="Monthly Cohort Retention (%)"
  colorScale={["#1e3a5f", "#2563eb", "#93c5fd"]}
/>

---

```sql pipeline_runs
SELECT
    pipeline_name,
    run_date,
    duration_seconds,
    status,
    rows_processed
FROM warehouse.pipeline_run_log
ORDER BY run_date DESC
LIMIT 500
```

<DataTable data={pipeline_runs} rows={20} sortable={true}>
  <Column id="pipeline_name" title="Pipeline" />
  <Column id="run_date" title="Run Date" fmt="datetime" />
  <Column id="duration_seconds" title="Duration" fmt="num1" suffix="s" />
  <Column id="rows_processed" title="Rows" fmt="num0" />
  <Column id="status" title="Status" contentType="colorscale" />
</DataTable>

Templated Pages and URL Parameters

Evidence supports parameterized reports through URL-driven filters and SvelteKit-style dynamic route files. A file named pages/customers/[customer_id].md generates one page per row in a specified query, enabling customer-level drill-down reports without a backend. The inputs component family provides dropdowns and date pickers that filter other queries on the same page through reactive variable binding.

# pages/customers/[customer_id].md
# Dynamic page — generates one page per customer_id at build time

```sql customers_list
-- This query drives page generation — one page per row
SELECT customer_id FROM warehouse.dim_customers WHERE is_active = true
```

```sql customer_detail
SELECT
    c.customer_id,
    c.company_name,
    c.customer_segment,
    c.first_order_at,
    c.lifetime_orders,
    c.lifetime_revenue_usd,
    c.account_manager
FROM warehouse.dim_customers c
WHERE c.customer_id = '${params.customer_id}'
```

```sql customer_orders
SELECT
    order_id,
    ordered_at,
    status,
    gross_revenue_usd,
    net_revenue_usd,
    acquisition_channel
FROM warehouse.fct_orders
WHERE customer_id = '${params.customer_id}'
ORDER BY ordered_at DESC
LIMIT 100
```

# {customer_detail[0].company_name}

**Segment:** {customer_detail[0].customer_segment} |
**Account Manager:** {customer_detail[0].account_manager} |
**Customer Since:** {customer_detail[0].first_order_at}

<BigValue data={customer_detail} value="lifetime_revenue_usd" title="Lifetime Revenue" fmt="usd0" />
<BigValue data={customer_detail} value="lifetime_orders" title="Total Orders" fmt="num0" />

## Order History

<DataTable data={customer_orders} sortable={true} rows={20}>
  <Column id="ordered_at" title="Date" fmt="date" />
  <Column id="status" title="Status" />
  <Column id="gross_revenue_usd" title="Revenue" fmt="usd2" />
  <Column id="acquisition_channel" title="Channel" />
</DataTable>
# pages/revenue/by-channel.md
# Interactive filtering with Inputs components — no backend needed

```sql channels
SELECT DISTINCT acquisition_channel AS label, acquisition_channel AS value
FROM warehouse.fct_orders
ORDER BY 1
```

<Dropdown
  data={channels}
  name="selected_channel"
  label="Acquisition Channel"
  value="value"
  title="label"
  defaultValue="organic"
/>

<DateRange
  name="date_range"
  label="Date Range"
  defaultValue="last_90_days"
/>

```sql filtered_revenue
SELECT
    ordered_at::date                    AS date,
    SUM(gross_revenue_usd)              AS revenue,
    COUNT(DISTINCT order_id)            AS orders
FROM warehouse.fct_orders
WHERE acquisition_channel = '${inputs.selected_channel}'
  AND ordered_at BETWEEN '${inputs.date_range.start}' AND '${inputs.date_range.end}'
GROUP BY 1
ORDER BY 1
```

<LineChart
  data={filtered_revenue}
  x="date"
  y="revenue"
  title="Revenue — {inputs.selected_channel}"
  yFmt="usd0k"
/>

Note

Inputs filtering in Evidence works at query-rerun time during the client-side hydration phase, not at SQL execution time. The full query result is embedded in the static bundle; Dropdown and DateRange values filter JavaScript arrays in the browser. For large result sets, use SQL-level WHERE clauses driven by inputs variables to push filtering to the build step and keep bundle sizes manageable.

dbt Integration and Semantic Layer

Evidence works naturally alongside dbt. In the most common pattern, dbt runs first (in CI or on a schedule), materializes mart-layer tables in the warehouse, and Evidence queries those tables directly. The dbt project provides tested, documented, version-controlled transformation logic; Evidence's build step then validates that every report query returns data with no runtime errors — making the combined pipeline testable end to end.

# Typical dbt + Evidence monorepo layout

data-platform/
├── dbt/                            # dbt project
│   ├── models/
│   │   ├── staging/
│   │   ├── intermediate/
│   │   └── marts/
│   │       ├── fct_orders.sql      # public mart models
│   │       ├── dim_customers.sql
│   │       └── agg_revenue_daily.sql
│   ├── dbt_project.yml
│   └── profiles.yml
│
└── evidence/                       # Evidence project
    ├── sources/
    │   └── warehouse/
    │       └── connection.yaml     # points at same warehouse as dbt prod target
    ├── pages/
    │   ├── index.md
    │   ├── revenue/
    │   │   └── overview.md         # queries dbt mart: fct_orders, agg_revenue_daily
    │   └── customers/
    │       └── [customer_id].md    # queries dbt mart: dim_customers, fct_orders
    └── evidence.config.yaml
# .github/workflows/deploy-reports.yml
# CI/CD: dbt runs first, then Evidence builds and deploys

name: Deploy Analytics Reports

on:
  push:
    branches: [main]
  schedule:
    - cron: "0 6 * * *"  # Daily refresh at 06:00 UTC

jobs:
  dbt-run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dbt
        run: pip install dbt-bigquery==1.8.0

      - name: dbt run
        working-directory: dbt
        env:
          GOOGLE_APPLICATION_CREDENTIALS_JSON: ${{ secrets.GCP_SA_KEY }}
        run: |
          echo "$GOOGLE_APPLICATION_CREDENTIALS_JSON" > /tmp/gcp-key.json
          export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-key.json
          dbt deps && dbt run --target prod --select "marts.*"

      - name: dbt test
        working-directory: dbt
        run: dbt test --target prod --select "marts.*"

  evidence-build:
    needs: dbt-run
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install dependencies
        working-directory: evidence
        run: npm ci

      - name: Build Evidence reports
        working-directory: evidence
        env:
          EVIDENCE_BIGQUERY_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}
          EVIDENCE_BIGQUERY_PROJECT_ID: my-gcp-project
        run: npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: analytics-reports
          directory: evidence/build

Custom Svelte Components

When built-in components are not sufficient, Evidence lets you write custom Svelte components in the components/ directory and use them in any .md page without importing. Custom components receive query result rows as typed props. You have full access to Svelte reactivity, D3.js, or any npm-installable charting library — Evidence places no restrictions on what your components can render.

<!-- components/PipelineStatusBadge.svelte -->
<!-- Custom component: renders a status badge with color coding -->
<script>
  export let status = "unknown";
  export let duration_seconds = 0;

  const colorMap = {
    success: "bg-green-500/20 text-green-400 border-green-500/30",
    failed:  "bg-red-500/20 text-red-400 border-red-500/30",
    running: "bg-blue-500/20 text-blue-400 border-blue-500/30",
    skipped: "bg-gray-500/20 text-gray-400 border-gray-500/30",
  };

  $: colorClass = colorMap[status] || colorMap.skipped;
  $: durationLabel = duration_seconds >= 60
    ? `${Math.floor(duration_seconds / 60)}m ${duration_seconds % 60}s`
    : `${duration_seconds}s`;
</script>

<span class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium {colorClass}">
  {status}
  {#if duration_seconds > 0}
    <span class="opacity-60">({durationLabel})</span>
  {/if}
</span>
# pages/operations/pipeline-health.md
# Using the custom PipelineStatusBadge component

```sql pipeline_status
SELECT
    pipeline_name,
    last_run_at,
    last_run_status,
    last_run_duration_seconds,
    avg_duration_7d,
    success_rate_7d
FROM warehouse.pipeline_monitoring
ORDER BY last_run_at DESC
```

## Pipeline Health Dashboard

<DataTable data={pipeline_status} rows={30} sortable={true}>
  <Column id="pipeline_name" title="Pipeline" />
  <Column id="last_run_at" title="Last Run" fmt="datetime" />
  <Column id="last_run_status" title="Status" contentType="component">
    <PipelineStatusBadge
      status={row.last_run_status}
      duration_seconds={row.last_run_duration_seconds}
    />
  </Column>
  <Column id="success_rate_7d" title="7d Success Rate" fmt="pct1" />
  <Column id="avg_duration_7d" title="Avg Duration" fmt="num1" suffix="s" />
</DataTable>

Deployment Options

Because npm run build produces a plain static site, Evidence deploys to any static hosting platform. For internal analytics with access control, the most common pattern is deploying to an S3 bucket or GCS bucket fronted by a load balancer with SSO authentication (Cloudflare Access, AWS Cognito, or Google IAP). Evidence Cloud is the hosted SaaS option: you connect your Git repo, add credentials, and Evidence runs scheduled builds and serves reports with built-in authentication.

# Dockerfile for self-hosted Evidence with Nginx + HTTP Basic Auth
# Build the Evidence site and serve it behind Nginx

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG EVIDENCE_SNOWFLAKE_USERNAME
ARG EVIDENCE_SNOWFLAKE_PASSWORD
ARG EVIDENCE_SNOWFLAKE_ACCOUNT
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
COPY .htpasswd /etc/nginx/.htpasswd
EXPOSE 80
# nginx.conf — serve Evidence static site with basic auth
events {}
http {
  include mime.types;
  server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    # Basic auth for internal access
    auth_basic "Analytics Platform";
    auth_basic_user_file /etc/nginx/.htpasswd;

    # SPA routing — fall back to index.html for client-side navigation
    location / {
      try_files $uri $uri.html $uri/ /index.html;
    }

    # Cache immutable hashed assets aggressively
    location /_app/immutable/ {
      add_header Cache-Control "public, max-age=31536000, immutable";
    }

    gzip on;
    gzip_types text/plain text/css application/javascript application/json;
  }
}
# Kubernetes CronJob: rebuild Evidence nightly and push to S3
apiVersion: batch/v1
kind: CronJob
metadata:
  name: evidence-nightly-build
  namespace: analytics
spec:
  schedule: "30 5 * * *"   # 05:30 UTC — after dbt completes at 05:00
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: evidence-builder
              image: node:20-alpine
              workingDir: /workspace
              command: ["/bin/sh", "-c"]
              args:
                - |
                  npm ci &&
                  npm run build &&
                  npx @aws-sdk/client-s3 sync build/ s3://analytics-reports/                     --delete                     --cache-control "max-age=3600" &&
                  aws cloudfront create-invalidation                     --distribution-id ${CF_DISTRIBUTION_ID}                     --paths "/*"
              env:
                - name: EVIDENCE_SNOWFLAKE_USERNAME
                  valueFrom:
                    secretKeyRef:
                      name: evidence-credentials
                      key: snowflake_username
                - name: EVIDENCE_SNOWFLAKE_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: evidence-credentials
                      key: snowflake_password
                - name: CF_DISTRIBUTION_ID
                  valueFrom:
                    secretKeyRef:
                      name: evidence-credentials
                      key: cloudfront_distribution_id
              volumeMounts:
                - name: source
                  mountPath: /workspace
          volumes:
            - name: source
              gitRepo:
                repository: https://github.com/myorg/analytics-reports.git
                revision: main

Production Checklist

1

Store all data source credentials in environment variables, never in connection.yaml files. Evidence reads connector credentials from env vars at build time. Use GitHub Actions secrets, Kubernetes Secrets, or Evidence Cloud's encrypted credential store. Never commit credentials to the repository.

2

Add Evidence build to your dbt CI pipeline as a downstream step. A successful dbt run that produces broken Evidence queries is a deployment failure. Run 'npm run build' after dbt test passes and fail the CI job if Evidence exits non-zero — this catches query errors, missing columns, and data type mismatches before they reach production.

3

Use named queries consistently and document them. Every SQL block on a page becomes a named variable. Short, descriptive names (daily_revenue, cohort_retention, pipeline_status) make pages readable as documents. Avoid generic names like data or rows — they collide across pages and confuse future editors.

4

Set showQueryEditor: false in production. The query editor is useful during development for iterating on SQL, but exposes your schema and partial data to authenticated users in production. Disable it unless you intentionally want analyst self-service.

5

Limit dynamic page generation to manageable cardinalities. A [customer_id].md template with 100,000 customers generates 100,000 HTML files at build time, consuming gigabytes of disk and minutes of build time. Add a WHERE clause to the page-generation query to limit to active or high-value entities, or switch to client-side filtering with Inputs components.

6

Cache Evidence builds in CI with the .evidence/ directory. Evidence caches compiled query results in .evidence/. Mount this directory between CI runs or use GitHub Actions cache with a key based on the dbt manifest hash — unchanged queries skip re-execution, cutting build time from minutes to seconds.

7

Pin connector package versions in package.json. Connector packages follow semver but breaking changes can slip through minor versions. Pin to exact versions ("@evidence-dev/bigquery": "2.3.1") and test upgrades in a PR before applying to main.

8

Use Evidence's built-in format strings instead of raw SQL rounding. Evidence format strings (usd0, pct1, num0k) handle locale-sensitive formatting in the browser. Doing rounding and formatting in SQL means the data is wrong for non-US locales and loses precision for future aggregations.

9

Add a page-level last_updated timestamp from a metadata table. Analytics users need to know how fresh the data is. A small SQL query selecting the max updated_at from a pipeline run log table, displayed in a BigValue component, tells readers exactly when the underlying data was last refreshed.

10

Validate report accessibility before sharing broadly. Evidence renders semantic HTML with ARIA attributes, but custom Svelte components bypass this. Use axe DevTools or Lighthouse on your built reports to catch color contrast failures and missing labels before distributing to stakeholders who rely on screen readers or high-contrast modes.

Maintaining analytics dashboards in Tableau or Power BI where metric changes require manual updates across dozens of reports, no version control on report definitions, or needing a lightweight self-hosted BI solution that deploys like a static website?

We design and implement Evidence.dev analytics platforms — from project scaffolding and data source connector configuration for DuckDB, BigQuery, Snowflake, and PostgreSQL to SQL report authoring with built-in chart and table components, templated drill-down pages with dynamic route generation, Inputs-driven interactive filtering, dbt pipeline integration with Evidence as the downstream reporting layer, GitHub Actions CI/CD pipelines that run dbt and then Evidence with automated warehouse credential injection, custom Svelte component development for bespoke visualization requirements, static site deployment on Cloudflare Pages or S3 with CloudFront, Kubernetes CronJob nightly rebuild automation, and Nginx or reverse-proxy auth integration for internal access control. Let’s talk.

Let's Talk

Related Articles

DataSOps Consulting

Need help implementing this in production?

We build and operate data pipelines, AI systems, and observability stacks for engineering teams. Reach out for a free 30-minute architecture review.