diff --git a/Dockerfile.backend b/Dockerfile.backend
new file mode 100644
index 0000000..4ff5605
--- /dev/null
+++ b/Dockerfile.backend
@@ -0,0 +1,14 @@
+# Stage 1: Build the backend binary
+FROM rust:1.82-slim AS builder
+RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
+WORKDIR /app
+COPY Cargo.toml Cargo.lock ./
+COPY crates/ crates/
+RUN cargo build --release --bin mass-driver-backend
+
+# Stage 2: Minimal runtime image
+FROM debian:bookworm-slim
+RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
+COPY --from=builder /app/target/release/mass-driver-backend /usr/local/bin/mass-driver-backend
+EXPOSE 3001
+CMD ["mass-driver-backend"]
diff --git a/Dockerfile.frontend b/Dockerfile.frontend
new file mode 100644
index 0000000..44ca84a
--- /dev/null
+++ b/Dockerfile.frontend
@@ -0,0 +1,24 @@
+# Stage 1: Build WASM package
+FROM rust:1.82-slim AS wasm-builder
+RUN apt-get update && apt-get install -y curl pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
+RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+WORKDIR /app
+COPY Cargo.toml Cargo.lock ./
+COPY crates/ crates/
+RUN wasm-pack build crates/mass-driver-wasm --target web --out-dir pkg
+
+# Stage 2: Build SvelteKit frontend
+FROM node:22-slim AS frontend-builder
+WORKDIR /app/frontend
+COPY frontend/package.json frontend/package-lock.json ./
+RUN npm ci
+COPY --from=wasm-builder /app/crates/mass-driver-wasm/pkg ./node_modules/mass-driver-wasm
+COPY frontend/ ./
+RUN npm run build
+
+# Stage 3: Serve with nginx
+FROM nginx:alpine
+COPY --from=frontend-builder /app/frontend/build /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/nginx.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/crates/mass-driver-backend/Cargo.toml b/crates/mass-driver-backend/Cargo.toml
index 931336a..0919160 100644
--- a/crates/mass-driver-backend/Cargo.toml
+++ b/crates/mass-driver-backend/Cargo.toml
@@ -4,13 +4,13 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-orbital-mechanics = { path = "../orbital-mechanics" }
-mass-driver-core = { path = "../mass-driver-core" }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
-sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
+sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono"] }
serde = { workspace = true }
serde_json = { workspace = true }
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
tracing = "0.1"
tracing-subscriber = "0.3"
+base64 = "0.22"
+chrono = { version = "0.4", features = ["serde"] }
diff --git a/crates/mass-driver-backend/src/db.rs b/crates/mass-driver-backend/src/db.rs
new file mode 100644
index 0000000..9eea89b
--- /dev/null
+++ b/crates/mass-driver-backend/src/db.rs
@@ -0,0 +1,152 @@
+use sqlx::PgPool;
+
+pub async fn init_db(pool: &PgPool) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ CREATE TABLE IF NOT EXISTS transfer_matrices (
+ config_hash VARCHAR(64) PRIMARY KEY,
+ station_count INTEGER NOT NULL,
+ launch_velocity_kms REAL NOT NULL,
+ matrix_bytes BYTEA NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ accessed_at TIMESTAMPTZ DEFAULT NOW()
+ );
+ "#,
+ )
+ .execute(pool)
+ .await?;
+
+ sqlx::query(
+ r#"
+ CREATE TABLE IF NOT EXISTS cached_routes (
+ id SERIAL PRIMARY KEY,
+ config_hash VARCHAR(64) REFERENCES transfer_matrices(config_hash) ON DELETE CASCADE,
+ from_station INTEGER NOT NULL,
+ to_station INTEGER NOT NULL,
+ route_json JSONB NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ UNIQUE(config_hash, from_station, to_station)
+ );
+ "#,
+ )
+ .execute(pool)
+ .await?;
+
+ tracing::info!("Database tables initialized");
+ Ok(())
+}
+
+pub async fn store_matrix(
+ pool: &PgPool,
+ config_hash: &str,
+ station_count: i32,
+ launch_velocity_kms: f32,
+ matrix_bytes: &[u8],
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"
+ INSERT INTO transfer_matrices (config_hash, station_count, launch_velocity_kms, matrix_bytes)
+ VALUES ($1, $2, $3, $4)
+ ON CONFLICT (config_hash) DO UPDATE
+ SET station_count = EXCLUDED.station_count,
+ launch_velocity_kms = EXCLUDED.launch_velocity_kms,
+ matrix_bytes = EXCLUDED.matrix_bytes,
+ accessed_at = NOW()
+ "#,
+ )
+ .bind(config_hash)
+ .bind(station_count)
+ .bind(launch_velocity_kms)
+ .bind(matrix_bytes)
+ .execute(pool)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn get_matrix(
+ pool: &PgPool,
+ config_hash: &str,
+) -> Result