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