Skip to content
This repository was archived by the owner on Mar 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
983d8ed
chore: add .worktrees to gitignore
Mar 21, 2026
72b6c85
feat(infra): add hetzner terraform variables and outputs
Mar 21, 2026
e470c34
fix(infra): require explicit ssh_allowed_ips — no open-world default
Mar 21, 2026
3ee056a
feat(infra): add hetzner terraform main configuration
Mar 21, 2026
241ca7e
feat(infra): add cloudflare DNS records for bdp.dev
Mar 21, 2026
316c3ac
feat(infra): add prod environment config and secrets template
Mar 21, 2026
1c4ef46
feat(infra): add dokploy admin bootstrap script
Mar 21, 2026
084d294
feat(infra): add shared cloud-init scripts (volume-mount, restic, ufw)
Mar 21, 2026
1b64c1d
feat(infra): update docker-compose for dokploy + minio + persistent v…
Mar 21, 2026
70b6472
feat(infra): add cloud-init for BDP production server
Mar 21, 2026
cf47010
feat(infra): add bootstrap script and gitignore for secrets/state
Mar 21, 2026
2bbf91f
docs(infra): update README for hetzner + dokploy setup
Mar 21, 2026
b170435
feat(infra): rewrite xtask infra module with full hetzner command set
Mar 21, 2026
28d1e6e
style(infra): terraform fmt main.tf
Mar 21, 2026
df13194
feat(infra): migrate to hetzner vps with dokploy + terraform
Mar 21, 2026
b3fecd6
fix(infra): align secrets handling with temnir pattern
Mar 21, 2026
96314b6
docs: add vector embeddings & /vectors page design spec
Mar 21, 2026
68c810d
docs: add WebGPU graph view design spec for 10M+ node biological know…
Mar 21, 2026
8825bcc
fix(infra): uppercase .secrets keys, add gen-secrets command
Mar 21, 2026
793d2f6
docs(vectors): add implementation plan for vector embeddings and /vec…
Mar 21, 2026
51a04de
feat(db): enable pgvector and add entry_embeddings table with HNSW index
Mar 21, 2026
b63b312
feat(db): add entry_projections and vector_projection_runs tables
Mar 21, 2026
6fd7c54
feat(bdp-embed): scaffold CLI + source-type-aware embed text builders
Mar 22, 2026
8ae0b51
feat(bdp-embed): add embed subcommand with OpenAI batching and increm…
Mar 22, 2026
87b1b4d
feat(bdp-embed): add project subcommand with landmark UMAP and model …
Mar 22, 2026
79b4f7a
feat(bdp-embed): add tiles subcommand with quadtree build and MinIO u…
Mar 22, 2026
b181c7a
fix(bdp-embed): correct quadtree downsampling formula to use 4^z LOD
Mar 22, 2026
a537b29
chore(server): add pgvector, async-openai, moka dependencies
Mar 22, 2026
cc8d569
feat(vectors): add get_stats query and vectors feature module skeleton
Mar 22, 2026
3a72e31
fix(vectors): preserve Option semantics for current_run_id in get_stats
Mar 22, 2026
08cd4ef
feat(vectors): add semantic_search and get_neighbors queries
Mar 22, 2026
a473ba4
feat(vectors): add get_tile handler, routes, and register all vector …
Mar 22, 2026
a7b58f9
feat(web): add source-type colors constant and vector tile loader
Mar 22, 2026
c5a9fc0
feat(web): add /vectors page with regl-scatterplot and tile-based loa…
Mar 22, 2026
a73bfac
feat(web): add vector sidebar, search bar, and header nav link
Mar 22, 2026
3769bf8
fix(web): replace invalid Scatter icon with ChartScatter from lucide-…
Mar 22, 2026
d4b49aa
docs(vectors): add test suite design spec for all four test layers
Mar 22, 2026
07a80b4
docs(vectors): fix spec issues from reviewer pass
Mar 22, 2026
d4fa4ed
docs(vectors): fix second spec reviewer pass — coordinate bug, ambigu…
Mar 22, 2026
d7a433d
docs(vectors): add implementation plan for vectors test suite
Mar 22, 2026
f25ed01
docs(vectors): fix plan Task 8 — use standalone in-process E2E, not d…
Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions .github/workflows/infrastructure.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# =============================================================================
# Infrastructure CI/CD - Terraform on Hetzner Cloud
# =============================================================================
#
# Security Model:
# - Secrets stored in GitHub Environment "production" (not repo secrets)
# - Secrets named TF_VAR_<key> — passed directly as env vars to Terraform
# - No .tfvars files — all variables via environment
# - PRs can only run `plan` (no apply)
# - Apply requires manual approval from maintainers
# - Fork PRs cannot access secrets or run workflows
#
# Required GitHub Setup:
# 1. Create Environment "production" in repo settings (Settings → Environments)
# 2. Add required reviewers for the production environment
# 3. Add secrets matching the keys in .secrets.example:
# TF_VAR_hcloud_token
# TF_VAR_ssh_public_key
# TF_VAR_ssh_allowed_ips
# TF_VAR_cloudflare_api_token
# TF_VAR_acme_email
# TF_VAR_dokploy_admin_password
# TF_VAR_minio_root_user
# TF_VAR_minio_root_password
# TF_VAR_restic_password

name: Infrastructure

run-name: "Infrastructure [${{ github.event.inputs.action || 'plan' }}] - production"

on:
pull_request:
paths:
- 'infrastructure/hetzner/**'
- '.github/workflows/infrastructure.yml'

workflow_dispatch:
inputs:
action:
description: 'Terraform action to perform'
required: true
type: choice
options:
- plan
- apply
- destroy
default: plan

confirm_destroy:
description: 'Type "destroy" to confirm destruction'
required: false
type: string

concurrency:
group: terraform-${{ github.ref }}
cancel-in-progress: true

env:
TF_VERSION: '1.7.0'
TF_DIR: 'infrastructure/hetzner/terraform'

jobs:
security-check:
runs-on: ubuntu-latest
outputs:
is_fork: ${{ steps.check.outputs.is_fork }}
steps:
- name: Check if fork
id: check
run: |
if [ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]; then
echo "is_fork=true" >> $GITHUB_OUTPUT
echo "::warning::Fork PR detected - infrastructure workflows disabled for security"
else
echo "is_fork=false" >> $GITHUB_OUTPUT
fi

# ---------------------------------------------------------------------------
# Plan — runs on PRs and manual trigger
# ---------------------------------------------------------------------------
plan:
name: Terraform Plan
runs-on: ubuntu-latest
needs: security-check
if: |
needs.security-check.outputs.is_fork == 'false' &&
(github.event_name == 'pull_request' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'plan'))
environment: production
env:
# Secrets are stored in GitHub as TF_VAR_<key> and passed directly
TF_VAR_hcloud_token: ${{ secrets.TF_VAR_hcloud_token }}
TF_VAR_ssh_public_key: ${{ secrets.TF_VAR_ssh_public_key }}
TF_VAR_ssh_allowed_ips: ${{ secrets.TF_VAR_ssh_allowed_ips }}
TF_VAR_cloudflare_api_token: ${{ secrets.TF_VAR_cloudflare_api_token }}
TF_VAR_acme_email: ${{ secrets.TF_VAR_acme_email }}
TF_VAR_dokploy_admin_password: ${{ secrets.TF_VAR_dokploy_admin_password }}
TF_VAR_minio_root_user: ${{ secrets.TF_VAR_minio_root_user }}
TF_VAR_minio_root_password: ${{ secrets.TF_VAR_minio_root_password }}
TF_VAR_restic_password: ${{ secrets.TF_VAR_restic_password }}
steps:
- uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: ${{ env.TF_DIR }}
run: terraform init

- name: Terraform Validate
working-directory: ${{ env.TF_DIR }}
run: terraform validate

- name: Terraform Plan
id: plan
working-directory: ${{ env.TF_DIR }}
run: |
terraform plan -no-color -out=tfplan 2>&1 | tee plan.txt
echo "plan_output<<EOF" >> $GITHUB_OUTPUT
head -200 plan.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true

- name: Comment PR with Plan
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Plan 📖
<details><summary>Show Plan</summary>

\`\`\`terraform
${{ steps.plan.outputs.plan_output }}
\`\`\`
</details>

*Triggered by: @${{ github.actor }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})

- name: Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1

- name: Upload Plan
uses: actions/upload-artifact@v4
with:
name: tfplan
path: ${{ env.TF_DIR }}/tfplan
retention-days: 5

# ---------------------------------------------------------------------------
# Apply — requires manual approval
# ---------------------------------------------------------------------------
apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: security-check
if: |
needs.security-check.outputs.is_fork == 'false' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.action == 'apply'
environment:
name: production
url: https://bdp.dev
env:
TF_VAR_hcloud_token: ${{ secrets.TF_VAR_hcloud_token }}
TF_VAR_ssh_public_key: ${{ secrets.TF_VAR_ssh_public_key }}
TF_VAR_ssh_allowed_ips: ${{ secrets.TF_VAR_ssh_allowed_ips }}
TF_VAR_cloudflare_api_token: ${{ secrets.TF_VAR_cloudflare_api_token }}
TF_VAR_acme_email: ${{ secrets.TF_VAR_acme_email }}
TF_VAR_dokploy_admin_password: ${{ secrets.TF_VAR_dokploy_admin_password }}
TF_VAR_minio_root_user: ${{ secrets.TF_VAR_minio_root_user }}
TF_VAR_minio_root_password: ${{ secrets.TF_VAR_minio_root_password }}
TF_VAR_restic_password: ${{ secrets.TF_VAR_restic_password }}
steps:
- uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: ${{ env.TF_DIR }}
run: terraform init

- name: Terraform Apply
working-directory: ${{ env.TF_DIR }}
run: terraform apply -auto-approve

- name: Show Outputs
working-directory: ${{ env.TF_DIR }}
run: |
echo "## Infrastructure Applied" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
terraform output >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY

# ---------------------------------------------------------------------------
# Destroy — requires manual approval + typed confirmation
# ---------------------------------------------------------------------------
destroy:
name: Terraform Destroy
runs-on: ubuntu-latest
needs: security-check
if: |
needs.security-check.outputs.is_fork == 'false' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.action == 'destroy' &&
github.event.inputs.confirm_destroy == 'destroy'
environment: production
env:
TF_VAR_hcloud_token: ${{ secrets.TF_VAR_hcloud_token }}
TF_VAR_ssh_public_key: ${{ secrets.TF_VAR_ssh_public_key }}
TF_VAR_ssh_allowed_ips: ${{ secrets.TF_VAR_ssh_allowed_ips }}
TF_VAR_cloudflare_api_token: ${{ secrets.TF_VAR_cloudflare_api_token }}
TF_VAR_acme_email: ${{ secrets.TF_VAR_acme_email }}
TF_VAR_dokploy_admin_password: ${{ secrets.TF_VAR_dokploy_admin_password }}
TF_VAR_minio_root_user: ${{ secrets.TF_VAR_minio_root_user }}
TF_VAR_minio_root_password: ${{ secrets.TF_VAR_minio_root_password }}
TF_VAR_restic_password: ${{ secrets.TF_VAR_restic_password }}
steps:
- uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: ${{ env.TF_DIR }}
run: terraform init

- name: Terraform Destroy
working-directory: ${{ env.TF_DIR }}
run: terraform destroy -auto-approve

- name: Summary
run: |
echo "## Infrastructure Destroyed" >> $GITHUB_STEP_SUMMARY
echo "All Hetzner resources have been destroyed (volume persists)." >> $GITHUB_STEP_SUMMARY
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# BDP .gitignore
# This file specifies intentionally untracked files that Git should ignore

# Git worktrees
.worktrees/

# ============================================================================
# Rust
# ============================================================================
Expand Down
7 changes: 7 additions & 0 deletions crates/bdp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ uuid = { workspace = true }
sha2 = { workspace = true }
reqwest = { workspace = true }

# ============================================================================
# Vector Search
# ============================================================================
pgvector = { version = "0.4", features = ["sqlx"] }
async-openai = "0.27"
moka = { version = "0.12", features = ["future"] }

# ============================================================================
# CQRS / Mediator
# ============================================================================
Expand Down
31 changes: 31 additions & 0 deletions crates/bdp-server/src/cqrs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,37 @@ pub fn build_mediator(pool: PgPool, storage: Storage) -> AppMediator {
async move { crate::features::protein_metadata::commands::insert::handle(pool, cmd).await }
}
})
// ================================================================
// Vectors
// ================================================================
.add_handler({
let pool = pool.clone();
move |query| {
let pool = pool.clone();
async move { crate::features::vectors::queries::get_stats::handle(pool, query).await }
}
})
.add_handler({
let pool = pool.clone();
move |query| {
let pool = pool.clone();
async move { crate::features::vectors::queries::semantic_search::handle(pool, query).await }
}
})
.add_handler({
let pool = pool.clone();
move |query| {
let pool = pool.clone();
async move { crate::features::vectors::queries::get_neighbors::handle(pool, query).await }
}
})
.add_handler({
let storage = storage.clone();
move |query| {
let storage = storage.clone();
async move { crate::features::vectors::queries::get_tile::handle(storage, query).await }
}
})
.build()
}

Expand Down
2 changes: 2 additions & 0 deletions crates/bdp-server/src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub mod query;
pub mod resolve;
pub mod search;
pub mod shared;
pub mod vectors;
pub mod version_files;

use axum::Router;
Expand Down Expand Up @@ -92,4 +93,5 @@ pub fn router(state: FeatureState) -> Router<()> {
.nest("/sync-status", jobs::sync_status_routes().with_state(state.clone()))
.nest("/files", files::files_routes().with_state(state.clone()))
.nest("/query", query::query_routes().with_state(state.clone()))
.nest("/vectors", vectors::vectors_routes().with_state(state.clone()))
}
4 changes: 4 additions & 0 deletions crates/bdp-server/src/features/vectors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod queries;
pub mod routes;

pub use routes::vectors_routes;
Loading
Loading