The HTTP + gRPC service that fronts the vectorless engine.
Expose the engine as a network service so that non-Go clients (SDKs, curl, the control plane, MCP adapters, agents) can use it. The server is a thin transport layer — it does request decoding, auth, and response encoding. It does not contain retrieval logic, parsing logic, or LLM logic.
vectorless-server. Separate repo from the engine. Imports the engine
as a Go module dependency.
Until Phase 2 extraction is done, the server code lives in
pkg/api/ inside the engine repo and compiles into the engine binary.
After extraction, it becomes its own binary.
- Terminates HTTP/1.1, HTTP/2, and gRPC on the same port.
- Decodes incoming requests into engine-typed Go values.
- Applies middleware: request ID, recovery, logging, metrics, optional API-key auth.
- Calls into the engine in-process.
- Encodes engine responses back over the wire.
- Graceful shutdown: stops accepting, drains in-flight, closes engine workers.
- Multi-tenancy. No org concept. No per-tenant quota. That's the control plane's job.
- Billing or usage metering. Ditto.
- Database migrations. The engine owns its schema. The server does not touch Postgres directly.
- Per-user rate limiting. Only a coarse global rate limit, if any.
One handler, three transports: gRPC, gRPC-Web, and plain
HTTP/JSON. All driven from the same .proto definitions.
Why Connect-RPC (github.com/bufbuild/connect-go):
- Single handler implementation, no code duplication between REST and RPC surfaces.
- HTTP/JSON path is standard — curl and Postman work with no tooling.
- gRPC path gets full streaming, codegen, and tooling for free.
- TypeScript SDK falls out of the same proto via
@connectrpc/connect-es. - Works behind any HTTP/2 proxy (Cloudflare, ALB, nginx).
The alternative — hand-written REST handlers plus a separate gRPC server — means two implementations of every endpoint drifting apart. Not worth it.
All routes versioned under /v1. Breaking changes ship as /v2
alongside a deprecation window.
POST /v1/documents— ingest a document. Multipart or JSON body. Returns202 Acceptedwith adocument_id.GET /v1/documents— list with keyset pagination (limit,cursor,status).GET /v1/documents/{id}— metadata + lifecycle status.GET /v1/documents/{id}/tree— the compactViewused for reasoning.GET /v1/documents/{id}/source— stream the original bytes. Optional.DELETE /v1/documents/{id}— cascades to sections.
GET /v1/sections/{id}— section metadata + full content from storage.
POST /v1/query— body:{ document_id, query, model?, max_tokens?, reserved_for_prompt?, max_parallel_calls?, max_sections? }. Returns{ document_id, query, strategy, model, sections[], elapsed_ms }.
GET /v1/health— liveness.GET /v1/version— build info.GET /metrics— Prometheus scrape.
POST /internal/jobs/{kind}— webhook surface for push-based queue drivers (e.g. QStash). Verifies signature.
The server has one pluggable auth mode. Default is off.
type Authenticator interface {
Authenticate(r *http.Request) (Principal, error)
}Shipped implementations:
NoAuth— always returns an anonymous principal. Default.StaticAPIKey(key string)— compares theAuthorization: Bearer ...header to a configured value withsubtle.ConstantTimeCompare. For self-hosters.
Config:
auth:
mode: "none" | "api_key"
api_key: "vls_live_..."The control plane supplies its own Authenticator implementation when
running in SaaS mode — one that validates against the control-plane
database. The server doesn't know or care what mode it's in.
Invariant: the engine never receives auth info. By the time a
request reaches the engine, it's already authorised. The engine
doesn't even have a principal parameter.
In order:
RequestID— generate or propagateX-Request-ID.RealIP— honourX-Forwarded-Forbehind a trusted proxy.Recoverer— convert panics into 500s with a logged stack trace.Logging— structured access log (method, path, status, duration).Metrics— Prometheus histograms + counters.Authenticator— skipped for/v1/healthand/v1/version.RateLimit(optional, per-key) — token bucket per principal.- The handler itself.
Each middleware is a func(http.Handler) http.Handler. Order matters:
recovery wraps everything so panics are always caught, request ID is
outermost so every log line has it.
On SIGTERM / SIGINT:
- Stop accepting new connections (
srv.Shutdown(ctx)). - Drain in-flight HTTP requests (15s timeout).
- Signal the engine's queue workers to stop.
- Wait for in-flight jobs to finish or timeout.
- Close the DB pool, the storage client, the queue driver.
- Exit 0.
The 15s drain matches Kubernetes' default terminationGracePeriodSeconds
so rolling deploys don't cut requests mid-flight.
Two modes, both supported:
- Plaintext (default). Terminate TLS at the reverse proxy (Caddy, nginx, ALB, ingress). This is what 90% of deploys want.
- Direct TLS. Opt in via
server.tls.{cert_file, key_file, min_version}config. For single-node deploys without a proxy.
Autocert / Let's Encrypt integration for single-node deploys is a future optional. Not in v1.
- Structured logging via
slog(JSON in prod, console in dev). Every log line includesrequest_id,document_idwhere relevant,principal_idafter auth. - Prometheus metrics at
/metrics:http_requests_total{method, path, status}http_request_duration_seconds{method, path}(histogram)queue_job_duration_seconds{kind, status}(histogram)llm_tokens_total{provider, model, direction=in|out}llm_request_duration_seconds{provider, model}(histogram)documents_ingested_total{status}
- OpenTelemetry tracing. Each HTTP request starts a root span; engine calls add child spans (parse, summarise, strategy.Select, LLM call). OTLP export to whichever collector the operator configures.
Same binary, two roles:
docker run vectorless-server:1.2.3 server # HTTP + gRPC + embedded workers
docker run vectorless-server:1.2.3 worker # queue workers only
For small deploys: one server container. For larger deploys: one
server container behind a load balancer + N worker containers on
an autoscaler driven by queue depth.
- Streaming queries over SSE or gRPC server-streaming. Connect-RPC
supports server-streaming natively. The handler layer is ready; the
engine strategy interface would need a
SelectStreamvariant. - Presigned-URL passthrough. When storage supports signed URLs
(S3, R2),
GET /v1/sections/{id}could return a URL the client fetches directly. Saves bandwidth on large sections. - Per-key rate limiting. Basic token bucket is cheap; needs a distributed counter (Redis) in multi-replica deploys.
- ENGINE.md — what the server imports.
- SDKS.md — what generates from the server's proto.
- CONTROL-PLANE.md — the layer above that
supplies an
Authenticatorand sits in front of the server in SaaS.